I can see value in being able to see all that happens when save is called in one place. Not sure it's worth the downsides though.
People have been railing (ha) against callbacks for years, and I used to be onboard with that. But I've come back around to them. They are pervasive through Rails libraries, so trying to avoid them seems a bit daft. You can look at the model and see the callback structure right there. And callbacks have a facility for wrapping up shared callbacks by using callback classes.
Service objects (otherwise known as making controllers fatter by putting what they're doing somewhere else although at least its reusable 😁) help as a place to put code that shouldn't be a callback.
Just checkout Trailblazer, solves the problems of callback hell. We want the full control of the order of calls in our business logic. As a developer I don't want business logic to be cumbersome to maintain and debug.
If you aren't quite ready to ditch callbacks but are looking for a tool to help you untangle gnarly models or at least update your tests to prevent more complexity from being added you might want to check out this gem I built for GitHub a few years ago (can't provide full link since I'm a new user to this platform but look up this on github): jonmagic/arca
We still use it in our test suite today to warn folks if they are about to introduce something that is considered more complex than we want to support.
Yes, callback hell is a thing, but no, I really wouldn't want to solve it by overriding the expected behaviour of ActiveRecord models. Instead use service objects that deal with business logic, or a route I'm now pursuing on a few projects: use ActiveJobs as "service objects", whilst service objects don't have to be run async, they perhaps can.
Of course you are right and thank you.
Majority of devs and teams makes decisions based on analogy (how somebody else did it) and not on "first principles" (what is logical and best for us).
I will bookmark this, and when that kind of people ask me "for a blog that describes what I am talking about", I will send this article URL.
If you have over seventy callbacks and custom validations, then the problem is hardly in the callbacks per se...
Moving them elsewhere doesn't solve the problem of complexity, it merely shoves it under the rug.
If you intend to use Commands, use commands, but do not monkeypatch the AR - callbacks may be bad, but monkeypatching is worse. This also violates principle of least surprise.
A better alternative might be Concerns. You get to hide'n'keep all the good stuff without needing to monkeypatch the guts.
Another alternative is to use a custom validator class that encapsulates and performs all the intended validations and a callback class handler which you call on a single
before_savecallback in the model itself.class Foo < ActionRecord::Base validates_with Validators::Foo before_save Callbacks::Foo.new endThat way you extract and encapsulate callbacks and validation and avoid monkeypatching the core AR class.
If you're having a hard time understanding your callbacks, your validations, and your code, consider adding comments and structuring/sorting/grouping them in the order they are executed. Yes, that requires discipline. Yes, shoving the problem under the rug is simpler that getting self-disciplined.
But.
There is one thing that people tend to forget about: you are not the first one working on this code, and you won't be the last. There will be other devs after you who will look at this and their WTF-per-minute meter will be going through the roof. Principle of Least Surprise all the way, all the way...