Settings

Theme

On logic in a Rails app, revisited 6 years later

alisnic.github.io

58 points by alisnic 7 years ago · 32 comments

Reader

archey1 7 years ago

Been doing Rails development for 6+ years. The most maintainable codebases I've worked on had some kind of service layer between the controller and the model. We used the "interactor" gem to create individual units of business logic that we could reuse and piece together into larger "flows". Business logic stayed in the interactors, persistence logic in the models. This lead to skinny controllers, skinny models and many, many many reusable skinny services. One fortunate side effect is that all these pieces became extremely easy to test in isolation, as well as integration tested.

https://github.com/collectiveidea/interactor

  • mercer 7 years ago

    I remember as I was working my way through the "Programming Phoenix" book, I felt that the whole idea of "context" modules seemed like an unnecessary indirection. Why put all those functions in a separate module instead of just doing the work in the controller? In my initial use cases I'd end up with an AccountController that just calls a function in the Accounts module which in turn uses the User module to actually interact with the database.

    Once I actually started building stuff and writing tests I had my aha-moment. By putting most of my logic in a separate 'context' module, I could both use and test all that without having to do all the bootstrapping that was necessary to run the web-framework controller logic.

    While this might seem obvious to some, for me it felt like a proper level-up as a developer, as I suddenly realized how the same principle could be applied in other contexts.

  • drchiu 7 years ago

    My team discovered the interactor pattern about 8 months ago and never looked back. It’s worked extremely well and easy to read. We’ve found it necessary to maintain a sensible naming convention for the interactors and to namespace them.

    The other pattern we’ve found to work decently well is to ensure most operations are idempotent. It makes it easy to ensure the correct state.

    • karmakaze 7 years ago

      What are some of the parts to your naming conventions if there are any prevailing patterns?

      • drchiu 7 years ago

        Currently we're experimenting with namespacing per "ownership" of that namespace.

        For instance, if the interactor is in charge of onboarding a new account, we have it under something like this:

        Account::SetupOnboarding

        Note that we also have an Account model here and so anything namespaced under it will be assumed to take place in that context.

kudokatz 7 years ago

A lot of discussion I heard around writing idiomatic Rails led to really fat models. Given the number of database trips back and forth with ActiveRecord, it didn't end well.

Eventually as the system grew it was much better to have bulk-interfaces of data-only for reads that completely bypassed ActiveRecord. Logic in controllers was also eventually factored out and re-used elsewhere.

My experience is that a lot of advice for Rails centers around small-to-medium size applications, and of course practices that are efficient and practical for apps of that size might not work exceptionally well in other scenarios.

  • skunkworker 7 years ago

    A number of years ago I started decoupling my business logic from my controllers and models and now use the ActiveInteraction gem. It really makes a difference when you have multiple interfaces that need to deal with a specific action on a specific object without having to call a controller all of the time.

    While idiomatic Rails is definitely possible, I've been bitten by coding errors in Models where specific validations are only ran on update and/or create. Now I just run a CreatePerson object which only has the specific functionality needed and is decoupled as much as possible from other sections.

x0x0 7 years ago

What jumped out at me was this:

> To give you some context, it was a time when I was starting to grow as a Rails/Ruby developer. I was reading a lot of blog posts on the topics and (as any young developer with a lot of self esteem) I started to have very strong feelings as to how Rails code should be written.

Lots of programming advice is like this -- people definitely make sure not to let being a beginner, or close to, stop them from lecturing people with a decade plus of experience building applications how to do things...

Railsify 7 years ago

We use this library to isolate business logic: https://github.com/cypriss/mutations

esaym 7 years ago

Related: The "AnemicDomainModel" https://www.martinfowler.com/bliki/AnemicDomainModel.html

In other words, there should be another layer in-between your DAO (data access object, ORM, etc) and controller. The "Model" in MVC was never meant to represent a single row of a database in object form. A Model should have a DAO but a Model should not be a DAO.

nurettin 7 years ago

spring boot enforces the idea of having a service layer between controller and model to act as a glue between the two, so the controller can focus on controlly things like authentication and handling request s/responses (http,ws,rmq,whatever) and the service can handle creating objects, reading/saving entities and making calculations.

It just makes things easier to digest because you know what resides where. If I returned to rails, I would be using this approach more.

rogem002 7 years ago

I'm currently building a Rails app with a event driven development (EDD) approach. This post makes me tingle.

After building apps with easily way to much logic in controllers, EDD feels like a much cleaner approach for long term scale. But finding the right EDD approach that feels like "the rails way" has sparked a lot of debate within our team, especially around the "does this actually help" argument.

I'm petty curious about others experiences :)

burlesona 7 years ago

I've found that a pretty simple technique along the lines of what's shared in the article makes complex Rails apps much more maintainable. Most of this applies to any MVC style app/framework.

I follow these rules of thumb:

1. Controllers should only handle converting HTTP to ruby calls. That includes logic that is specific to the request flow, like parsing params or authenticating cookies, but nothing else.

2. Models should only handle read/write on their own table. You can use associations, but no referencing another class name inside of a model. No after_* callbacks (and try not to use callbacks at all).

3. What Rails calls "views" should be though of as simple html templates with loops and simple if/else, but no complex logic.

4. Don't use Rails Helper Methods. Just don't.

All by itself this works for toy apps, but now you've got holes where complex presentational and procedural logic has no place to go. So you plug those gaps with two kinds of domain objects: view objects (for complex reads) and action objects (for complex writes).

View Objects (more often called Presenters in Rails land to avoid the conflict with the templates, which rails calls "views"):

These are used to wrap up any kind of complex, multi-model view. So for example, when you have something like an "account settings" page, you probably need to fetch the user and some associated models, maybe billing info, etc. You can make a simple object that takes in URL params in its constructor, efficiently queries whatever is needed to present this page, then freezes. Now you can put whatever data and logic is needed for the template here, and it's very easy to unit test the queries and the individual bits of logic to ensure they're correct.

Action Objects (sometimes called Mutations, Commands, Interactions, Services, or Procedures):

These are used to wrap up any kind of mutative procedure. They should take in a set of inputs, and when called, perform some kind of action (for example, running through all the steps of user registration). These should be written functionally, and should be idempotent whenever possible. Again, wrapping the code up this way makes it very easy to unit test, and to stub in external dependencies when relevant.

These patterns make it really easy to follow what's going on in your app - easy to add new behavior and easy to walk through complex business processes step by step since everything happens in one control flow. And of course you can compose these objects together for the most complex flows. The simple and stable interfaces help keep your program easy to reason about and allow you to work on individual parts in isolation with confidence.

I've used those patterns over the last ten years or so with great success, and more recently have been helping my team at Atlassian gradually convert what was a somewhat messy older Rails app. Happy to answer questions if anyone has any :)

— Edit —

Just to add, there’s one more big benefit, which is that if you code this way it becomes trivial to replicate any of the behavior in your app from the Rails console. Of course this is the same reason it’s easy to test when you build this way, and writing tests is more important than poking around in the console. But when I’ve worked with people who aren’t as in love with testing as I am, I’ve found that they get more excited when I show them how this puts all your apps behavior into an interface that’s very easy to drive from the console. :)

  • Railsify 7 years ago

    Great write up, seasoned devs who pick up rails do this right away, junior devs who pick up rails rewrite their app using the principles after the ratio of "reading existing spaghetti code:writing new code" starts pushing deadlines.

  • mercer 7 years ago

    > 3. What Rails calls "views" should be though of as simple html templates with loops and simple if/else, but no complex logic.

    I rather like how Phoenix makes a clear separation between views and templates. The templates have no logic, but the views can have quite a bit of it as long as it is directly linked to the templates (presentational).

    • richjdsmith 7 years ago

      When I first looked at Phoenix ~18 months ago, I was a fairly fresh developer and couldn't figure out the separation of Views and Templates (coming from Rails).

      I'm just now going through the Programming Phoenix 1.4 book and it's just been one 'aha' moment after the other, with the separation of logic and views being one of the most significant.

  • faizshah 7 years ago

    Really interesting comment, thanks for sharing your experience. Can you expand on why Rails helper methods are bad?

    I'm just starting out my Rails journey so I haven't encountered those issues yet.

    • burlesona 7 years ago

      Welcome to Rails world! It’s a fun place to work :)

      Why no helpers:

      1. The scoping is weird and encourages bad patterns.

      - You have access to the instance variables from whatever controller action happened to call the particular helper, but you can’t see that when writing the helper or in the view, so it’s very opaque. God forbid you actually USE the instance variables and you’ve now got these weird fragments of tightly coupled code between your controller and your template that quickly become hard to reason about.

      - It’s better if you require that all helper methods be strictly functional, but in that case it’s still unintuitive because your methods are mixed into every view. This can lead to surprising name collisions and all kinds of other weirdness.

      2. They’re not easy to test due to the issues above.

      3. They’re weird to include elsewhere - for example if you want to reference in a model.

      3. Most important: the principal value of common utility functions is to have the sort of “general purpose operations” defined in exactly one place, well tested, and then utilized everywhere else.

      For all of these reasons you’re better off defining modules and using ‘module_function’ to make the methods directly accessible. Then you can just call the methods like ‘Utils.foo_bar()’ wherever you need them.

      • faebi 7 years ago

        I always had exactly one usecase for an application helper when using bootstrap: „<%= glyphicon(:cloud) %>“ It‘s way faster than rendering templates, should be available anywhere and is truly convenient.

andrew_wc_brown 7 years ago

I've been using Rails since version 0.8.6 and have apps as old as 8 years I'm still maintaining. I've had teams as large as 20 on a single rails codebase.

You do not need anything other than the MVC pattern.

I would love to see the claimants of these needed abstractions post their code so I could refactor and show you how you're wrong.

If you make good use of your base controllers you can reduce controller code to next to nothing.

If you avoid Rails abstractions such as scopes use callbacks sparingly and focus on writing raw SQL instead of using Arel's query builder your models are easy to manage.

  • matthewowen 7 years ago

    20 is not a big team. I work as part of a 100+ org on a rails codebase, and I know that isn't especially large.

miki123211 7 years ago

The original post sounds very much like what Robert Martin is proposing.

revskill 7 years ago

Sharing business logic between models/controllers/views often leads to a mess.

There's a reason to just write business logic code at only ONE place, the controller.

You'll thank yourselves years later when you revisited your code. Just one place to look for.

  • conanbatt 7 years ago

    Long term controllers being fat will give you headaches if you want to provide an api for different platforms (mobile, another web)

  • drizze 7 years ago

    But placing logic in the controllers means you need to get a rails controller to test your business logic. This may seem fine in a small application, but as an app grows it becomes a headache to need to tie all your business rules to your framework.

    “Rails is not your application”

    • revskill 7 years ago

      If "Rails it not your application", why use Rails then ?

      • joevandyk 7 years ago

        It’s a set of libraries that work great together, it’s pretty easy to learn, and lots of people know Rails.

ravenstine 7 years ago

I think it all depends on where the logic is placed. Controllers shouldn't have much business logic in them, but moving logic out of a controller or a model isn't necessarily better if it's done in an obfuscationary way.

The problem is that the patterns provided by Rails and suggested by the Rails community encourage "needless indirection" more than they do actually managing logic in a sane way. Concerns, though they definitely useful for some things, end up becoming dumping grounds for loosely-coupled application logic. I know that the same could be said for just creating modules without ActiveSupport::Concern, but the existence of ActiveSupport::Concern seems to have suggested to a lot of Rails developers that the de facto answer to fat controllers and models is to just dump excess logic into these "concern" modules.

ActiveRecord in itself is another fundamentally flawed concept in Rails because it treats data and the interface to that data as one in the same. Models become dumping grounds for a ton of seemingly data-oriented behavior that probably better exist as helpers. I like the idea of ActiveRecord, and I seriously loved it when I first learned Rails, but every Rails project I've encountered contains these needlessly fat models with lots of overridden/custom attributes(that could have been helpers), callback after callback, etc.

An alternative example exists in Ember Data, where the data and interface are split between concepts: The "model", the "adapter", and the "serializer". This keeps the logic around data very well organized and interoperable(i.e. switching adapters). Models in Ember can still have custom attribute getters, but I still say that it's best to try to avoid those if possible and instead look to creating helper functions first.

I guess the point of what I'm saying is that Rails developers think too much in terms of object-orientation, which leads them down the path of thinking about relationships in a way that encourages bloat. In other words, the mindset becomes that where if some behavior has to do with the concept of a Post, for instance, then that behavior should belong in the Post class(without taking much account into whether that code only involves the view or data persistence). Logically, it makes sense from an OO point of view, but then you're going to end up with a pile of code in one place that you will inevitably extract into a "concern" which you'll have to "include" in other models that share said behavior.

Often times, just creating a set of functions/methods is simpler and more understandable. They don't have to be a part of a specific model or a class, but just be available when needed. In Rails, most of the time that's the view, sometimes the controller, so helpers in Rails are perfect for that. Helpers are contained in modules, but with the way they are integrated into the application, you don't have to think much about that.

Business logic that is more complex or falls outside the scope of helpers should go into Ruby code that doesn't depend on Rails, but can just be imported and used within a Rails app. This not only creates a separation between business logic and the logic of rendering HTTP responses, but refraining from making all your code Rails-centric means that the business logic should be easier to test in isolation and faster without all the overhead of Rails.

jsjkkkkkkkkk 7 years ago

Rails is opinionated and forces a literal sense of MVC pattern, its dogmatic, thus the fat controller or fat model phenomena

  • archy_ 7 years ago

    Yep, it encourages some very bad practices and is really inefficient. My team dropped it years ago, even for prototyping, and our remaining Rails apps are considered legacy and we've been slowly replacing them.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection