Stop abusing Rails callbacks

Reasons I do not like ActiveRecord callbacks:

  • They are confusing

`before/after_commit` is called when creating, updating, destroying an object;

`before/after_save` is called when creating and updating an object;

 When creating an object, `before_save` is called before `before_create` and `after_save` is called before `after_create`… And the list goes on. This really confuses me sometimes, if I do not pay too much attention, I might use some of them wrong.

  • They are implicit

It happened to me a couple times, when I tried to create or update a record, some callbacks are triggered unexpectedly. This really irritates me.

In most of the cases when you try to “do something after an active record object is created or updated”, using callbacks could be considered as the code smell.

To prevent it, I like to use `tap`  to “do something” explicitly.

Here is an example:

Say we have a user model:

class User < ActiveRecord::Base
  after_create :do_something

  def do_something
    puts 'do something...'
  end
end

when I create a new user, `do_something` callback will be triggered. While in most situation it work, however creating user object is tightly coupled with `do_something` method. It causes troubles when you don't mean to call `do_something` but you forgot this callback was defined in your model.

To refactor it, let’s remove `after_create` callback, and use `tap` to call `do_something` method explicitly.

# app/models/user.rb
class User < ActiveRecord::Base
  def do_something
    puts 'do something...'
  end
end

# app/controllers/users_controller.rb
class UsersController < ActionController::Base
  def register
    user = User.create(user_params).tap do |user|
      user.do_something
    end
  end
end
This basically does the same as `after_create`. You may ask what about `before_create`?

So ActiveRecord's `create` method can take a block, when you need to perform a `before_create` action, simply pass a block to do it.

For example:

class UsersController < ActionController::Base
  def register
    user = User.create(user_params) do |user|
      puts 'this is called before creating'
      user.some_attribute = 'value'
      user.do_something
    end
  end
end

When a block is passed to `create` method, ActiveRecord yield a value `User.new(user_params)`, which is user, a `User` instance in this case, we can set attribute to it, call model methods and other operations based on your domain logic, after that, `user.save` will be called. By this means, we will be able to do a `before_create` callback explicitly.