DEV Community

Discussion on: Business logic in Rails with operators

Collapse
alg profile image
Aleksey Gureiev

I have to second what Petr says. Business logic should never go inside a model. It often involves juggling with different models, external services, jobs and whatnot. Are you suggesting to push it all into models so that a post (for example) had the knowledge of some email sending functionality that it had to invoke when it's created? Absolutely not. Models are mostly DTOs providing their data manipulation and transformation as additional methods.

Then where does the logic go if it's not in the models? Who can have this knowledge of everything business-involved in your system? Controllers? No. And here's why. Controllers are there for processing your HTTP requests -- converting inputs, invoking operations and rendering outputs. Assume that you have other channels your requests for operations come in -- CLI, Websockets, queues... Are you going to duplicate your business logic across all of them? The sane architectural decision is to move all your business logic into a separate layer between your input channels (Rails controllers, for example) and your data layer. Here's where PORO, service objects, interactors, commands (however you call them) come into play.

Of course, everyone is entitled to do whatever they like with their apps. Unfortunately with modern languages and everyone jumping on the cool coding train without classical software engineering education we have a lot of programmers with no idea how to properly architecture their products. My suggestion to everyone looking to make themselves a better programmer is to take time to read works by Martin Fowler, Kent Beck, Robert Martin and others. Time well spent. Peace!

Collapse
jaredcwhite profile image
Jared White

So…you wrote this comment as if I have no idea what you're talking about. I never said a database record should itself be responsible for sending an email. Trust me, I've read all the same sources as you.

I think the disconnect here is: "Models are mostly DTOs"

Nope, I strongly disagree with this assertion. Models are literally your application's domain model. And your domain model might encompass a wide variety of tasks and business logic, including sending emails, generating reports, and anything else your app needs to do. Of those model objects in your repository, a portion will be responsible for connecting to database resources in particular. But that's just one type of model.

Rails' app/models folder isn't app/database_records for a reason. While an ActiveRecord object is a model object which uses the ActiveRecord pattern, that doesn't mean a model object ipso facto is an ActiveRecord pattern. You mentioned "PORO, service objects, interactors, commands" …yes indeed, I put those and many other OOP patterns in app/models with appropriate namespacing and descriptive terms based on what tasks they perform and how they relate to other model objects.

Or as an alternative, you can write some of them as if they were a separate library (whether you use lib or literally create a standalone gem), which lets you define your cleanly-separated API accordingly. I wish many more Rails developers were eager to create gems or gem-like code patterns so logic isn't so tightly coupled to existing Rails classes. But I digress.

In summary, the whole point of object-oriented programming is to combine data with the behaviors which act upon that data. Splitting your application's primary data (stored in models) from its behavior (function-like service "objects" or whatever that live entirely elsewhere) results in what Martin Fowler calls the Anemic Domain Model antipattern. Not a state of affairs we should promote. martinfowler.com/bliki/AnemicDomai...

Thread Thread
alg profile image
Aleksey Gureiev • Edited on

Good discussion. I replied to your comment without making any assumptions, so there's nothing personal. Made a specific effort not to rely anything said to anyone. And I never suggested to you specifically to read these sources, but to anyone willing to improve their coding-fu. I'm glad we share the library ticket. Now back to the subject.

As for database records sending emails, where did you get that? I was referring to the models. They don't do that. It's not their responsibility (SRP principle). In my systems, their responsibility lies in working with own data. You can hardly call Rails ActiveRecords and ActiveModels anemic as they provide a ton of functionality on top of plain DTOs.

The term "business logic" is so broad that we must be specific about what we call it. When I disagree with placing it into the models I mainly think about business operations. We got used to putting these into app/actions (named by controller "actions", the meat of which was extracted into their own classes). We also have app/services for general-purpose single-responsibility services for low-level operations.

Controllers in our apps look mostly like:

class ChatsController < ...
  def create
    authorize! Chat, to: :create?

    input = convert_input.(Chats::CreateInput, params)
    chat = create_chat.(input, user: current_user)

    respond_with chat
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we shift non-HTTP stuff out of controller into business operations layer where Chats::CreateChatAction belongs. The added benefit of all this is if you ever thought of cutting your monolithic app into now-modern microservices, you already have your code nicely separated into contexts, but that is a different story.

Hope I made my point clear. Just to reiterate, there's no such thing as ideal and true architecture. We attempt to make our systems as manageable, extensible and decoupled as we can. Moving out business operations into a separate layer helps us a lot.

Thread Thread
jaredcwhite profile image
Jared White • Edited on

You:

Business logic should never go inside a model.

Rails:

The Model layer represents the domain model (such as Account, Product, Person, Post, etc.) and encapsulates the business logic specific to your application. In Rails, database-backed model classes are derived from ActiveRecord::Base. Active Record allows you to present the data from database rows as objects and embellish these data objects with business logic methods. Although most Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by the Active Model module.

Source:
github.com/rails/rails/blob/main/R...

I think I shall trust what Rails says models are in Rails. 🙂 If you want to extend your own architecture beyond the "Rails way", that's certainly your prerogative, but for any folks out there creating POROs in app/models or even simply using ActiveRecord objects to encapsulate business logic, you are doing exactly what Rails says you should do. I rest my case.