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.
On logic in a Rails app, revisited 6 years later
11–20 of 33 posts
Re: On logic in a Rails app, revisited 6 years later
#12Related: 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.
The whole point of the Active Record pattern is that domain objects double as DAOs. https://martinfowler.com/eaaCatalog/activeRecord.html
Re: On logic in a Rails app, revisited 6 years later
#13The 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.
Re: On logic in a Rails app, revisited 6 years later
#14There'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.
Re: On logic in a Rails app, revisited 6 years later
#15Sharing 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.
“Rails is not your application”
Re: On logic in a Rails app, revisited 6 years later
#16I 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. :)
Re: On logic in a Rails app, revisited 6 years later
#17Re: On logic in a Rails app, revisited 6 years later
#18I'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…
Re: On logic in a Rails app, revisited 6 years later
#19Sharing 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.
Re: On logic in a Rails app, revisited 6 years later
#20Been 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 m…
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.