Concerned about Code Reuse?
19 Apr 2012Right out of the gate, Ruby gives us some powerful ways to re-use instance and class methods without relying on inheritance. Modules in Ruby can be used to mixin methods to classes fairly easily. For example, we can add new instance methods using include
.
module DogFort
def call_dog
puts "this is dog!"
end
end
class Dog
include DogFort
end
Now we’re able to call any methods defined in our DogFort
Module as if they were simply slipped into (included) into our Dog
class.
dog_instance = Dog.new
dog_instance.call_dog
# => "this is dog!"
Using Modules a fairly easy way to re-use methods, if you want you can extend
a Module to add methods to a class directly.
module DogFort
def board_the_doors
puts "no catz allowed"
end
end
class Dog
extend DogFort
end
Now if we were to call Dog.new.board_the_doors
we would get an error, since we’ve added it as a class method instead.
Dog.board_the_doors
# => "no catz allowed"
Dog.class
# => Class
Sweet! Though what if you wanted to add an instance method and a class method to a class. We could have two Modules, one to be included and one to be extended, wouldn’t be to hard but it would be nice if we only had to use one include statement, especially if the two Modules are related. So is it possible to add instance and class methods with only one include statement? Of course…
Enter Concerns
A concern is a Module that adds instance methods (like Dog.new.call_dog
) and class methods (like Dog.board_the_doars
) to a class. If you’ve poked around the Rails source code you’ll see this everywhere. It’s so common that Active Support added a helper Module to create concerns. To use it require ActiveSupport and then extend ActiveSupport::Concern
require 'active_support/concern'
module DogFort
extend ActiveSupport::Concern
# ...
end
Now any methods you put into this Module will be instance methods (methods on a new instance of a class Dog.new
) and any methods that you put into a Module named ClassMethods
will be added on to the class directly (such as Dog
).
require 'active_support/concern'
module YoDawgFort
extend ActiveSupport::Concern
def call_dawg
puts "yo dawg, this is dawg!"
end
# Anything in ClassMethods becomes a class method
module ClassMethods
def board_the_doors
puts "yo dawg, no catz allowed"
end
end
end
So now when we add this new Module to a class, we’ll get instance and class methods
class YoDawg
include YoDawgFort
end
YoDawg.board_the_doars
# => "yo dawg, no catz allowed"
yodawg_instance = YoDawg.new
yodawg_instance.call_dawg
# => "yo dawg, this is dawg!"
Pretty cool huh?
Included
That’s not all, Active Support also gives us a special method called included that we can use to call methods during include time. If you add included
to your ActiveSupport::Concern
any code in there will be called when it is included
module DogCatcher
extend ActiveSupport::Concern
included do
if self.is_a? Dog
puts "gotcha!!"
else
puts "you may go"
end
end
end
So when we include DogCatcher
in a class it’s included block will be called immediately.
class Dog
include DogCatcher
end
# => "gotcha!!"
class Cat
include DogCatcher
end
# => "you may go"
While this is a contrived example, you can imagine wanting to maybe make a concern for Rails controllers and wanting to add before_filter
's to our code. We can do this easily adding the included block.
Is this magic?
Nope, under the hood we’re just using good old fashioned Ruby. If you want to learn more about all the fun things you can do with Modules I recommend checking out one of my favorite Ruby books Metaprogramming Ruby and Dave Thomas also has a fantastic screencast series.
Gotcha
When you’re writing Modules I guarantee that you’ll slip up and accidentally try to create a class method using self
or class << self
but it won’t work because it’s now a method on the Module.
module DogFort
def self.call_dog
puts "this is dog!"
end
ene
In the example above the context of self
is actually the Module object DogFort
so when we include it into another class we won’t see the method.
class Wolf
include DogFort
end
Wolf.call_dog
# NameError: undefined local variable or method `call_dog'
wolf_instance = Wolf.new
wolf_instance.call_dog
# NameError: undefined local variable or method `call_dog'
If you want to use that method in this context you will need to call the Module directly
DogFort.call_dog
# => "this is dog!"
puts DogFort.class
# => Module
Fin
That’s all for today, in my next post I’m going to show you how to clean up your legacy code base with concerns. Let me know if you have any questions @schneems!
You may also be interested in Concerning Yourself with ActiveSupport::Concern, Concerns in ActiveRecord and Better Ruby Idioms.