
On logic in a Rails app, revisited 6 years later - alisnic
https://alisnic.github.io/posts/rails-logic-revisited/
======
archey1
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](https://github.com/collectiveidea/interactor)

~~~
drchiu
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
What are some of the parts to your naming conventions if there are any
prevailing patterns?

~~~
drchiu
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
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
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
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
We use this library to isolate business logic:
[https://github.com/cypriss/mutations](https://github.com/cypriss/mutations)

------
esaym
Related: The "AnemicDomainModel"
[https://www.martinfowler.com/bliki/AnemicDomainModel.html](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.

~~~
knubie
The whole point of the Active Record pattern is that domain objects double as
DAOs.

[https://martinfowler.com/eaaCatalog/activeRecord.html](https://martinfowler.com/eaaCatalog/activeRecord.html)

~~~
nullwasamistake
I agree. Not a fan of Martin Fowler for many reasons but a good ORM is your
DAO. Otherwise there's not much a point

------
nurettin
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
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
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. :)

~~~
mercer
> 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
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.

------
andrew_wc_brown
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
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
The original post sounds very much like what Robert Martin is proposing.

------
revskill
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.

~~~
drizze
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
If "Rails it not your application", why use Rails then ?

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

------
ravenstine
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
Rails is opinionated and forces a literal sense of MVC pattern, its dogmatic,
thus the fat controller or fat model phenomena

~~~
archy_
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.

