« HE:labs
HE:labs

Exploring Attribute Methods

Postado por Thiago Borges em 30/05/2014

During the development process, it is very common to explore some Ruby code using CTags. Mostly to see some documentation, also to understand how the method works under the hood and learn some tricks. During this process, I found out about ActiveModel::AttributeMethods and I'll explore it using TDD on this post.

First of all, what Attribute Methods is?

It is module that provides an easy way to create prefixed and suffixed methods. For example on ActiveModel::Dirty, which is implemented using Attribute Methods:

1 person = Person.new(name: 'Peter')
2 person.name = 'Bob'
3 person.name          # => "Bob"
4 person.name_changed? # => true
5 person.name_was?     # => "Peter"
6 person.name_change   # => ["Peter", "Bob"]
7 person.reset_name!   # => "Peter"

All these _changed?, _was?, _change and reset_ ! are set using the facilities of Attribute Methods.

Why should I care about it?

In the example above, the only attribute is name. But what if I have to implement all that prefix, suffix and affix for many attributes like email, phone, address? It would be a hell of duplication, which means a big chance of bugs and inconsistency.

To implement something like Dirty module, we have to implement the requirements:

  1. Include ActiveModel::AttributeMethods in your class.
  2. Call each of its method you want to add, such as attribute_method_suffix or attribute_method_prefix.
  3. Call define_attribute_methods after the other methods are called.
  4. Define the various generic _attribute methods that you have declared.

Now that we all know how it behaves and what is necessary, we can start the process. I'm assuming the project is set up with Active Model and RSpec, just to simplify the example.

Example

The idea is to implement a method _taken? that checks if the attribute is included on a predefined resource. It simple returns true if the attribute value already exists or false if it doesn't.

As we know what we want, let's create the first tests

 1 describe Person do
 2   describe 'check existent attributes' do
 3     context 'name' do
 4       it 'says Thiago name is already taken' do
 5         person = Person.new(name: 'Thiago')
 6         expect(person.name_taken?).to be_true
 7       end
 8 
 9       it 'says Gumercindo name is not taken' do
10         person = Person.new(name: 'Gumercindo')
11         expect(person.name_taken?).to be_false
12       end
13     end
14   end
15 end

Nice, based on this rspec test, I can drive the design of my Person class.

As mentioned above, I need 4 steps to setup a Attribute Method based class.

 1 require 'active_model'
 2 class Person
 3   include ActiveModel::AttributeMethods  # Step 1
 4   attr_accessor :name
 5 
 6   attribute_method_suffix '_taken?'      # Step 2
 7   define_attribute_methods %w(name)      # Step 3
 8 
 9    def initialize(params)
10      @name = params[:name]
11    end
12 
13   def attribute_taken?(attr)             # Step 4     def name_taken?(name)
14     send("#{attr}_collection").include?(send(attr)) #   name_collection.include?(name)
15   end                                               # end
16 
17   private
18     def name_collection
19       %w(Thiago Anézio Paulo Camila)
20     end
21 
22  end

On ordinary Rails projets, you don't need to worry about the first line, since Rails preloads all gems.

The tricky part is using metaprogramming to call attribute specific methods, like on line 14, which calls the private method #name_collection.

Now that the tests are passing, I can create the test for the email attribute:

 1 describe Person do
 2   describe 'check existent attributes' do
 3     context 'email' do
 4       it 'says thiago@gmail.com is already taken' do
 5         person = Person.new(email: 'thiago@gmail.com')
 6         expect(person.email_taken?).to be_true
 7       end
 8 
 9       it 'says gomercindo@example.com is not taken' do
10         person = Person.new(email: 'gomercindo@example.com')
11         expect(person.email_taken?).to be_false
12       end
13     end
14   end
15 end

And it is very simple to extend the class for the email attribute:

 1 require 'active_model'
 2 class Person
 3   include ActiveModel::AttributeMethods
 4   attr_accessor :name, :email
 5 
 6   attribute_method_suffix '_taken?'
 7   define_attribute_methods %w(name email)
 8 
 9   def initialize(params)
10     @name = params[:name]
11     @email = params[:email]
12   end
13 
14   def attribute_taken?(attr)                        # def name_taken?(name)
15     send("#{attr}_collection").include?(send(attr)) #   name_collection.include?(name)
16   end                                               # end
17 
18   private
19     def name_collection
20       %w(Thiago Anézio Paulo Camila)
21     end
22 
23     def email_collection
24       %w(thiago@gmail.com anezio@uol.com.br paulo@ig.com.br camila@terra.com.br)
25     end
26 end

You can check the diff here.

It is important to have name conventions to implement reusable code like this, and attribute methods guide us to this direction.

Conclusion

It is always important to keep the code as DRY as possible. Here at HE:labs we use CodeClimate to point the bad smells, like duplication, complexity, and churn. We also have 100% test coverage to help us refactoring with confidence.

This kind of refactoring is recommended when the whole team is confortable with this approach and in some cases it will become harder to change if the attributes start diverging the shared behavior. If this day come, your tests will be there to support you.

The code of this example is on github.

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!