Thursday, July 17, 2008

Ruby: Redefine Method Behavior Conditionally

I was recently working on the integration between Mocha and expectations. Expectations promotes the idea that you should only have one expectation per test; therefore, I wanted to display a warning if you call Object#expects within an expected block.

For example, the following code will print a warning.

Expectations do
expect 1 do
Object.expects(:something).returns 1
Object.something
end
end
# >> Expectations allows you to to create multiple mock expectations, but suggests that you write another test instead.
# >> expects method called from /Users/jay/example.rb:6
# >>
# >> Expectations .
# >> Finished in 0.001 seconds
# >>
# >> Success: 1 fulfilled

Usually, I'd use one of the various Alternatives for Redefining Methods, but redefining the Object#expects method had two additional constraints that complicated matters.At first I attempted to solve this problem by aliasing methods and pointing to different method definitions based on the context in which the code was currently executing. This turned into a complicated mess, and also required me to define a few methods that could potentially collide with methods defined by applications using Expectations. After going down that path for a bit it became clear that it would be simpler to define modules and delegate the expects call to the appropriate module based on the context.

The actual implementation involves several moving parts, so here's a much simpler example. Start with some behavior defined on Object. This behavior will have been defined by another framework so you cannot (easily) alter the original method definition.

class Object
def say_hello
"hello"
end
end

Next, you've decided to create a framework that says hello in Spanish also. You still want to be able to return "hello" when English is required, but you want the say_hello method to return "hola" when you are expecting Spanish.

Below is the output we are looking for.

in_english do
say_hello # => "hello"
end

in_spanish do
say_hello # => "hola"
end

Currently our code returns "hello" both in English and Spanish.

# Framework Object::say_hello
class Object
def say_hello
"hello"
end
end

# Your Object class
class Object
def in_english(&block)
instance_eval(&block)
end

def in_spanish(&block)
instance_eval(&block)
end
end

in_english do
say_hello # => "hello"
end

in_spanish do
say_hello # => "hello"
end

We can make the say_hello message sent to Object return "hola" by removing the say_hello method, defining an InSpanish module, and including the InSpanish module.

# Framework Object::say_hello
class Object
def say_hello
"hello"
end
end

# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish

remove_method :say_hello

def in_english(&block)
instance_eval(&block)
end

def in_spanish(&block)
instance_eval(&block)
end
end

in_english do
say_hello # => "hola"
end

in_spanish do
say_hello # => "hola"
end

Now we have Spanish working, but we've lost our English. Remember the actual implementation needed to preserve the original behavior in some circumstances. The in_english method is our circumstance where we need to preserve original behavior. This can be done easily enough by Moving the say_hello definition from Object to an InEnglish module.

# Framework Object::say_hello
class Object
def say_hello
"hello"
end
end

# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish

module InEnglish
expects_method = Object.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end
include InEnglish

remove_method :say_hello

def in_english(&block)
instance_eval(&block)
end

def in_spanish(&block)
instance_eval(&block)
end
end

in_english do
say_hello # => "hello"
end

in_spanish do
say_hello # => "hello"
end

Now we have the original behavior of the say_hello method, but we've lost our ability to speak Spanish.

The final step is to define Object#say_hello in a way that delegates the say_hello message to the appropriate module instead of removing the method.

# Framework Object::say_hello
class Object
def say_hello
"hello"
end
end

# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish

module InEnglish
expects_method = Object.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end
include InEnglish

def say_hello
(@language || InEnglish).instance_method(:say_hello).bind(self).call
end

def in_english(&block)
@language = InEnglish
instance_eval(&block)
end

def in_spanish(&block)
@language = InSpanish
instance_eval(&block)
end
end

in_english do
say_hello # => "hello"
end

in_spanish do
say_hello # => "hola"
end

The final change was setting the @language instance variable to the module who's say_hello method definition was required. As you can see from the printed output, our code works as desired.

This isn't a technique that you'll use often, but it's a good trick to know when you need it. If you're interested in the actual application you can check out the expectations framework code.

Labels: ,




Tuesday, July 15, 2008

Ruby: Move a Method from a Class to a Module Definition

I was recently working with a framework that reopened and defined a method on Object. I wanted the behavior of the framework, but I also wanted to define my own behavior. This is generally the case for alias_method_chain or one of the other Alternatives for Redefining Methods, but my circumstances (to be discussed in a subsequent post) prevented me from using one of the better known solutions.

The solution that worked for me was to move the original method definition to a module.

class Speaker
def say_hello
"hello"
end
end

module EnglishSpeaker
expects_method = Speaker.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end

class Speaker
undef say_hello
end

Speaker.new.say_hello # => -:18: undefined method `say_hello' for #<Speaker:0x285b4> (NoMethodError)
Speaker.new.extend(EnglishSpeaker).say_hello # => "hello"

You may never need this technique, I only needed it once in the past 2.5 years. But, when it applied I found it to be significantly better than the alternatives.

Labels: , ,




Tuesday, April 08, 2008

Extend modules instead of defining methods on a metaclass

In the entry Replace method_missing with dynamic method definitions I have the following example code.

class Decorator
def initialize(subject)
subject.public_methods(false).each do |meth|
(class << self; self; end).class_eval do
define_method meth do |*args|
subject.send meth, *args
end
end
end
end
end

The context of the example can be summarized as, you want to delegate from the instance all the public methods defined on the constructor argument.

Ali Aghareza pointed out to me that defining methods on the metaclass of an instance isn't the nicest thing to do. The problem with it is that you've made it much harder for anyone else to change the behavior of the instance.

Here's a more simplified example. The following code creates a new Object and defines the hello_world method on the Object instance.

class Object
def metaclass
class << self; self; end
end
end

obj = Object.new
obj.metaclass.class_eval do
def hello_world
"hello"
end
end

obj.hello_world # => "hello"

This works fine; however, if someone wanted to change the way hello_world behaved, by defining the method on the metaclass you force them to make their change by redefining the method on the metaclass. The current solution does not allow you to extend modules and alter the behavior of the instance.

The following example demonstrates that extending a module does not change the behavior of an instance if the behavior has been defined on the metaclass.

class Object
def metaclass
class << self; self; end
end
end

obj = Object.new
obj.metaclass.class_eval do
def hello_world
"hello"
end
end

obj.hello_world # => "hello"

module Spanish
def hello_world
"hola"
end
end

obj.extend Spanish

obj.hello_world # => "hello"

A better solution is to change the behavior of the instance by extending modules instead of defining behavior on the metaclass.

obj = Object.new

module English
def hello_world
"hello"
end
end

obj.extend(English).hello_world # => "hello"

Now that the behavior is defined on an ancestor instead of the metaclass you can change the behavior by extending another module.

obj = Object.new

module English
def hello_world
"hello"
end
end

obj.extend(English).hello_world # => "hello"

module Spanish
def hello_world
"hola"
end
end

obj.extend(Spanish).hello_world # => "hola"

This solution works fine for our simple example, but it can also be applied to our first (much more complicated) example, even without knowing how to define the module. In the case of the Decorator, you can simply define an anonymous module and immediately extend it.

class Decorator
def initialize(subject)
mod = Module.new do
subject.public_methods(false).each do |meth|
define_method meth do |*args|
subject.send meth, *args
end
end
end
extend mod
end
end

Labels: , , ,




Monday, April 07, 2008

Alternatives for redefining methods

Ruby's open classes allow you define and redefine behavior pretty much at will; unfortunately, almost every option comes with caveats.

The example below is a gateway class that defines a process method. For the purposes of the example, assume that we need to redefine the process method on Gateway itself and call the original process method.* Also, assume that Gateway is not our class, so we cannot easily alter the original definition of process.

class Gateway
def process(document)
p "gateway processed document: #{document}"
end
end

Gateway.new.process("hello world")
# >> "gateway processed document: hello world"

Solution 1: alias

The following example uses alias to redefine the process method.

class Gateway
alias old_process process
def process(document)
p "do something else"
old_process(document)
end
end

Gateway.new.process("hello world")
# >> "do something else"
# >> "gateway processed document: hello world"

The example above creates an alias (old_process) for the process method. With an alias in place you can redefine the process method to anything you want and call the old_process method using the alias. This is probably the easiest solution and the most commonly used solution.

Unfortunately, it's not without problem. First of all, if someone redefines old_process you will get unexpected behavior. Second of all, the old_process method is really nothing more than an artifact of the fact that you have no other way to refer to the original method definition. Lastly, if the code is loaded twice, an infinite loop is created that causes the always painful to see 'stack level too deep' error.

Solution 2: alias_method_chain
Like I said, solution 1 is the most popular way to redefine a method. In fact, it's so popular Rails defines the alias_method_chain method to encapsulate the pattern. From the Rails source above alias_method_chain:
Encapsulates the common pattern of:
#
# alias_method :foo_without_feature, :foo
# alias_method :foo, :foo_with_feature
#
# With this, you simply do:
#
# alias_method_chain :foo, :feature
#
# And both aliases are set up for you.
#
# Query and bang methods (foo?, foo!) keep the same punctuation:
#
# alias_method_chain :foo?, :feature
#
# is equivalent to
#
# alias_method :foo_without_feature?, :foo?
# alias_method :foo?, :foo_with_feature?
#
# so you can safely chain foo, foo?, and foo! with the same feature.
Using alias_method_chain we can define our Gateway as the example below.

class Gateway
def process_with_logging(document)
p "do something else"
process_without_logging(document)
end
alias_method_chain :process, :logging
end

Gateway.new.process("hello world")
# >> "do something else"
# >> "gateway processed document: hello world"

Using alias_method_chain is nice because it's something familiar to many Rails developers. Unfortunately, it also suffers from the same problems as using alias on your own.

Solution 3: Close on an unbound method
The following code uses the class method "instance_method" to assign the "process" method (as an unbound method) to a local variable. The "process_method" local variable is in scope of the closure used to define the new process method, so it can be used within the process definition. Calling an unbound method is as simple as binding it to any instance of the class that it was unbound from and then using the call method.

class Gateway
process_method = instance_method(:process)
define_method :process do |document|
p "do something else"
process_method.bind(self).call(document)
end
end

Gateway.new.process("hello world")
# >> "do something else"
# >> "gateway processed document: hello world"

I've always preferred this solution because it doesn't rely on artifact methods that may or may not collide with other method definitions. Also, if the code is loaded multiple times the behavior is altered multiple times, but I find that easier to diagnose than when my only clue is "stack level too deep".

Unfortunately, this solution is not without flaws. Firstly, it relies on the fact that define_method uses a closure and has access to the unbound method. Of course this also implies that you have a handle on anything else defined in the same context. As with any closure, it's possible to accidentally create a memory leak. Also, (in MRI) I'm told that define_method takes 3 times as long to execute when compared to def.

Solution 4: Extend a module that redefines the method and uses super
This solution relies on creating a module with the new behavior and extending an instance with the module. Since the module is extended from the instance it will be checked first for the method definition when "process" is called (because it's the first ancestor). Since the module is the first ancestor it can use super to execute the process method defined in Gateway (the second ancestor).

module ProcessLogging
def process(document)
p "do something else"
super
end
end

Gateway.new.extend(ProcessLogging).instance_eval("class << self; self; end").ancestors
# => [ProcessLogging, Gateway, Object, Kernel]
Gateway.new.extend(ProcessLogging).process("hello world")
# >> "do something else"
# >> "gateway processed document: hello world"

This solution is my favorite because I can use def and super and never worry about creating any memory leaks or artifact methods.

Of course, it assumes that you get the opportunity to extend instances of the class. However, I haven't found that to be a problematic requirement.

* There are generally other options such as delegation, defining hooks, etc. Often I find these to be cleaner solutions and try that route first. But, sometimes red