3 reasons to avoid Rails callbacks
I remember when I was starting and saw the Active Record callbacks for the first time: “Wow they are so useful!” - I thought at the time. Oh god, how I was wrong.
I mean, they seem useful, and indeed they are useful for the first weeks. But then your application starts to grow and what before was pure joy becomes a series of headaches that culminate in your precious time being wasted in a lot of different ways.
In this post we will see why callbacks can hurt your code and one good way to avoid them.
1. They hide your code behavior
1 2 3
class Egg < ActiveRecord::Base before_save :hatch end
So simple, so dangerous.
It may seem obvious for us right now that a Egg will hatch before being saved to the database, but a developer in the future (you included) could be working on other parts of the code and forget that whenever an Egg gets saved it will hatch.
Imagine that the feature this developer is working needs the eggs unhatched. Yes, he may be in for a very unpleasant surprise. And all because this behavior was hidden from him.
2. It’s hard to get around them later
Ok, let’s keep on the Egg scenario: at the time of the implementation, it made perfect sense that any Egg should be hatched before being saved on the database.
But in software, as in life, things change. A lot. All the time. And the time came when a feature needed to save an unhatched Egg. Now the rails callbacks are standing in your way.
Well, of course we could do something like that:
1 2 3 4 5 6 7 8 9
class Egg < ActiveRecord::Base before_save :hatch def hatch if self.hatchable? self.status = :hatched end end end
But keep stacking up those ifs and you will see how chaotic a method can become.
3. They make your tests harder to write and mantain
Then things end up like this:
1 2 3 4 5 6 7 8 9 10
class Egg < ActiveRecord::Base before_save :hatch def hatch if self.hatchable? self.status = :hatched self.bird = Bird.create(species: self.species) end end end
So now you have eggs creating birds out of callbacks. Go grab some aspirin. This is sort of an extreme example, because this is SUCH a bad idea. But let’s keep going with it.
So you decide to test this thing, and you have some extra work handling edge cases, like testing the scenario of an Egg that was saved in the database before the bird feature was available.
Then everything works and you think: life is beautiful, right? Wrong.
You notice your tests slowing down. That is what you get when create birds every time you save an egg (that means one more round of ActiveRecord magic and one more operation in the database, both of which take their price in the tests performance).
Meet an alternative to Rails callbacks
Of course I would be a jerk if I ended this post telling you to avoid callbacks without giving you at least one good alternative, right?
They are one of my favorite “unofficial” abstractions in wide use on the Ruby on Rails community. In the future I wish to talk more about them, but for now I give you this excellent article from the Engine Yard blog that explains it quite nicely.
There you have it, the next time you think about adding an ActiveRecord callback, be sure to know what you are doing.