Intend to extend (metaprogramming in Ruby)
In this post I want to illustrate the different ways of how to extend Ruby classes and instances. As being a former Java developer it is always impressive, but also sometimes a bit confusing how many ways are available in Ruby to achieve that. There is already a lot written about this topic (see resources below), but as it is such an essential Ruby topic it is always nice to see things from different views.
Inheritance
One of the most common ways to add further functionality to an already existing class is to inherit from it and add the new stuff to the inherited class. Ruby isn’t here much different than any other object oriented language.
class Lifeform def eat puts "Yam" end end class Dog < Lifeform def eat puts "Yummi " + super end def bark puts "Wuff" end end Lifeform.new.eat # Yam Lifeform.new.bark # NoMethodError Dog.new.eat # Yummi Yam Dog.new.bark # Wuff
Merge methods in existing classes
But in Ruby you can also add new methods to a existing classes by just “reopening” the class and adding the definitions. In the below case Time is already defined in the Ruby standard library.
class Time def jump "Two thousand years later!" end end Time.now.jump # Two thousand years later!
Redefine methods of existing class
This approach also allows to redefine already existing methods. The hour method of the Time class is already defined in the Ruby standard library.
class Time def hour puts "Too late my friend." end end Time.now.hour # Too late my friend.
The downside by doing it this way is to loose the possibility to access the former functionality (no, the super method does not help here). If you still need to access the former defined method then you can use alias_method.
class Time alias_method :old_hour, :hour def self.now "Your hour, my lord: #{old_hour.to_s}. end end Time.now.hour # Your hour, my lord: 9.
For class methods you have to use alias_methos in the following way.
class Time class << self alias_method :old_now, :now end def self.now "The time my lord: #{old_now.to_s}. end end Time.now # The time my lord: Fri Mar 11 01:15:23 +0100 2011.
For a nice explanation of what class << self is all about just read this nice answer at Stackoverflow.
Define a method just for one instance
You can also add a method to just a single instance. All other instances of the class won’t have the method then.
class Lifeform end dog = Lifeform.new def dog.bite "grrr" end cat = Lifeform.new dog.bite # grrr cat.bite # NoMethodError
An alternative way to define the same method is by using the above mentioned class << X syntax.
class << dog def bite "grrr" end end dog.bite # grrr
Include a module
In Ruby you can mix modules into classes (in contrast to inheritance as many as you like). You can inherit and add modules at the same time (I guess that why it is also called mixins). When you include a module you make the module methods available to the instances of a class.
module Lifeform def eat puts "Yam" end def self.sleep puts "Zzz" end end class Worm include Lifeform end Worm.new.eat # Yam Lifeform.sleep # Zzz Worm.sleep # NoMethodError Lifeform.eat # NoMethodError Lifeform.new # NoMethodError
Note that in the above example sleep is defined as a module function and so could be used directly (without mixing the module into a class or instance).
Extend a class with a module
When you extend a class by using a module you make the methods of the module available to the class itself.
module Lifeform def eat puts "Yam" end end class Worm extend Lifeform end Worm.eat # Yam Worm.new.eat # NoMethodError
You can also extend the class outside of the class definition:
Worm.extend(Lifeform)
Extend an instance with a module
You can also extend an instance with a module. The module methods are then only available to this single instance.
module Lifeform def eat puts "Yam" end end class Worm end worm1 = Worm.new worm1.extend(Lifeform) worm1.eat # Yam worm2 = Worm.new worm2.eat # NoMethodError
Note that including a module into an instance is as far as I know not possible. It would not make sense as an instance can’t be instantiated again (only classes can be instantiated), and so include would not have any effect.
Include and extend the Rails plugin way
In Ruby On Rails there is a paradigm that is used by a lot of plugins (especially those acts as something ones). It is rather complicated done in the Rails Guide. Yehuda mentions better solutions on his blog. Here is a way I personally prefer.
module ActsAsCounter # class method def acts_as_lifeform include InstanceMethods end module InstanceMethods def count counter++ save end end end ActiveRecord::Base.extend ActsAsCounter
The above code adds the acts_as_lifeform method to Active::Record::Base as a class method. When this method now is called in one of the Rails model classes (that inherit from ActiveRecord::Base) then the count method is added to all instances of the model class. If you want to do something like that then I recommend to take also a closer look at ActiveSupport::Concern.
Extend at runtime with class_eval on class
In Ruby you can also dynamically extend classes and instances at runtime. class_eval evaluates the provided code block in the context of a class and thereby allows to attach methods to the class and all instances.
class Worm end Worm.class_eval do def eat puts "Yam" end def self.sleep puts "Zzz" end end Worm.new.eat # Yam Worm.sleep # Zzz Worm.eat # NoMethodError Worm.new.sleep # NoMethodError
Extend at runtime with instance_eval
In contrast instance_eval evaluates the provided code block in the context of an instance.
class Worm end charlie = Worm.new charlie.instance_eval do def eat puts "Yam" end end charlie.eat # Yam Worm.eat # NoMethodError
But you can also use it on a class. In that case the class is treated as an instance and the methods are directly attached (as class methods).
class Worm end Worm.instance_eval do def eat puts "Yam" end end Worm.eat # Yam Worm.new.eat # NoMethodError
A short memory crutch for class_eval and instance_eval could be (from Brian Morearty’s blog): * Use ClassName.instance_eval to define class methods. * Use ClassName.class_eval to define instance methods.
Benefit of extending at runtime
If you still wonder what the benefit of this is, then look at this code snippet from the ActsAsTaggable.
class_eval %( def #{tag_type}_list tag_list_on('#{tags_type}') end def #{tag_type}_list=(new_tags) set_tag_list_on('#{tags_type}', new_tags) end def all_#{tags_type}_list all_tags_list_on('#{tags_type}') end )
I don’t want to to go into details what exactly happens here. But as you see class_eval can also get a simple string. It is fully evaluated as Ruby code and can extend the class runtime (the string could be generated at runtime). That way there are nearly no limits at all.
The method_missing method
There is another way to define new methods without defining them … nope, I didn’t drink too much yet … just a bit red wine
When you call a non existent method it normally raises a NoMethodError. You can change that behavior in the predefined method_missing method and even put some logic or call other methods from there. I normally try to avoid this as it makes the code harder to read, but it can give some really convenient accessibilities (something like the find_by_something methods in Rails).
class Worm def method_missing(m, *args, &block) if (m.to_s == "eat") puts "Yam" else super end end def self.method_missing(m, *args, &block) if (m.to_s == "sleep") puts "Zzz" else super end end end Worm.new.eat # Yam Worm.slepp # Zzz Worm.new.sleep # NoMethodError Worm.eat # NoMethodError
Resources
- Explanation of
class << fooat Stackoverflow - Better Ruby Idioms
- Shining Ruby: Extending Classes
- Include vs Extend in Ruby
- Ruby Pattern: Extend through Include
- Fun with Ruby’s instance_eval and class_eval
- Ruby Method Missing
- Understanding class_eval, module_eval and instance_eval
- Class and Instance Methods in Ruby