We built an(other) object factory module for our current project and it looks a lot like all the others:
Update: Read the follow-up post Second thoughts on initializing modules
I was recently presented the problem of appending to the initialize method from a module that was being included. To do this it would need to override the class’s initialize method with my own but keep the functionality of the original initialize method.
Whenever I need to do something in Ruby that I know will require some experimentation I like to move outside of my application and reproduce the problem in a simple way. For this problem I created a Person class that mixes in a Teacher module.
module Teacher def initialize puts "initializing teacher" end end class Person include Teacher def initialize puts "initializing person" end end
The goal is to get the following output when a Person object is created:
> Person.new initializing teacher initializing person
The basic program fails as expected;
Teacher.new prints “initializing person” because Person’s initialize is trumping Teacher’s. Our immediate goal is to replace Person’s initialize with Teacher’s but in a way that preserves the original initialize method. By using
alias_method we can create a copy of the original initialize method that we can call later.
module Teacher def self.included(base) base.class_eval do alias_method :original_initialize, :initialize def initialize puts "initializing teacher" original_initialize end end end end
This solution is the simplest thing that could possibly work, unfortunately it also has one major limitation. For it to work the call to
include Teacher in Person has to come after Person’s definition of initialize. This is may be fine in situations where you have total control over the Person class, but what if Teacher is going to be part of a library you are distributing? Asking your users to place the include line to your module in a specific spot is unacceptable.
To make this work we need to be able to capture definitions of the method we want to redefine even after our module has been included. This sounds like a good time to use Ruby’s method_added hook.
module Teacher def self.included(base) base.extend ClassMethods base.overwrite_initialize base.instance_eval do def method_added(name) return if name != :initialize overwrite_initialize end end end module ClassMethods def overwrite_initialize class_eval do unless method_defined?(:custom_initialize) define_method(:custom_initialize) do puts "teacher initialized" original_initialize end end if instance_method(:initialize) != instance_method(:custom_initialize) alias_method :original_initialize, :initialize alias_method :initialize, :custom_initialize end end end end end
Whoa! As you can see a lot of complexity has been added to Teacher. However, what it’s doing is actually really cool. Here is the breakdown:
What self.included is doing:
- The ClassMethods module containing
overwrite_initializeis added to base (Person).
- overwrite_initialize is invoked.
- method_added is defined on Person at the class level.
What overwrite_initialize does:
- If a method called
custom_initializedoes not exist it defines one.
custom_initializeruns Teacher’s initialize logic and then defers to Person’s initialize.
- If the current
initializemethod is not our
initializeis preserved as
original_initializeand a copy of
custom_initializeis made to replace
What method_added is doing:
- Watches for new methods with the name “initialize”.
- When an initialize method is defined method_added calls
overwrite_initializeto put the chain from custom_initialize to this new initialize method in place.
What is particularly nice is that this implementation is flexible enough to handle multiple redefinitions of initialize. This is important because a subclass of Person may also define initialize. It is not perfect though—if the initialize in the subclass of Person calls
super the program will go into an infinite loop where
custom_initialize and the subclass’s
initialize call each other indefinitely. If anyone has a suggestion on how to get around this please post a comment or fork the gist on Github.
Read the follow-up to this post Second thoughts on initializing modules.