Root of Evil

Talking about ruby and other stuff.

Callbacks Extraction

| Comments

The unbridled use of the go to statement has an immediate consequence that it becomes terribly hard to find a meaningful set of coordinates in which to describe the process progress.

Rails provide powerful callback technique and as a powerful tool it could injure in newbie hands.

Understanding the problem

For example we have simple blog engine. Where we have Posts, Users, Comments And system should create a default post with random comments when User with type admin created. This could be done through rails callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class User < ActiveRecord::Base
  after_create :create_post, if: :admin?

  private
  def create_post
    posts << Post.create(default_post_attributes)
  end
end

class Post < ActiveRecord::Base
  after_create :create_comments

  private
  def create_comments
    comments << Comment.create(defaul_comment_attributes)
  end
end

class Comment < ActiveRecord::Base
  before_create :ensure_user_existance

  private
  def ensure_user_existance
    unless user
      user = User.create(default_user_attrubutes)
    end
  end
end

After some time passed you decide that creating user for comments was a bad idea and you remove it. But when you create another one admin you need to search through three different objects to find out why your default post does not have comments. This is the simplest example but you could find even more weird problems and real hell in practice.

Solution

For extracting this behavior out of objects we need to keep in mind some of best practices. Sandi Matz recommends to keep your objects unitary and easy to follow check out this blog post for more details.

So lets introduce new object that for now will encapsulate logic around admin user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class AdminUser
  def save
    user.save
    create_post
  end

  private
  def create_post
    posts << Post.create(comments: [Comment.create(user: user)])
  end
end

class User < ActiveRecord::Base
end

class Post < ActiveRecord::Base
end

class Comment < ActiveRecord::Base
end

This way we could extract any kind of callbacks out of User class that belongs to specific type of user of certain conditions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AdminUser
  attr_reader :attributes

  def initialize(options = {})
    before_initialize
    attributes = options[:user]
    after_initialize
  end

  def save
    before_save
    user.save
    after_save
  end

  private
  def user
    User.new(attributes)
  end

  #...
end

I recommend to always keep your objects as clean and small as possible, so any changes that you need to introduce touch single class that easy to understand and follow.

Resume

Extracting callbacks out of Domain Class could be useful refactoring technique and any of us should use to avoid callback hells and code smells. Unitary class is much easier to test and change.

Comments