Tuesday, March 18, 2008

Ruby: Isolate Dynamic Receptor

Isolate Dynamic Receptor

A class utilizing method_missing has become painful to alter

Introduce a new class and move the method_missing logic to that class.

Motivation

As I previously mentioned, objects that use method_missing often raise NoMethodError errors unexpectedly, or worse you get no more information than: stack level too deep (SystemStackError).

Despite the added complexity, method_missing is a powerful tool that needs to be used when the interface of a class can not be predetermined. On those occasions I like to use Isolate Dynamic Receptor to limit the behavior of an object that also relies on method_missing.

The ActiveRecord::Base (AR::B) class defines method_missing to handle dynamic find messages. The implementation of method_missing allows you to send find messages that use attributes of a class as limiting conditions for the results that will be returned by the dynamic find messages. For example, given a Person subclass of AR::B that has both a first name and a ssn attribute it's possible to send the messages Person.find_by_first_name, Person.find_by_ssn, and Person.find_by_first_name_and_ssn.

It's possible, but not realistic to dynamically define methods for all possible combinations of the attributes of an AR::B subclass. Instead utilizing method_missing is a good solution; however, by defining method_missing on the AR::B class itself the complexity of the class is increased significantly. AR::B would benefit from a maintainability perspective if instead the dynamic finder logic were defined on a class whose single responsibility was to handle dynamic find messages. For example, the above Person class could support find with the following syntax: Person.find.by_first_name, Person.find.by_ssn, or Person.find.by_first_name_and_ssn

Note: very often it's possible to know all valid method calls ahead of time, in which case I prefer Replace Dynamic Receptor with Dynamically Define Method.

MechanicsExample

Here's a recorder class that records all calls to method_missing.

class Recorder
instance_methods.each do |meth|
undef_method meth unless meth =~ /^(__|inspect)/
end

def messages
@messages ||= []
end

def method_missing(sym, *args)
messages << [sym, args]
self
end
end

The recorder class may need additional behavior such as the ability to play back all the messages on an object and the ability to represent all the calls as strings.

class Recorder
def play_for(obj)
messages.inject(obj) do |result, message|
result.send message.first, *message.last
end
end

def to_s
messages.inject([]) do |result, message|
result << "#{message.first}(args: #{message.last.inspect})"
end.join(".")
end
end

As the behavior of Recorder grows it becomes harder to understand what messages are dynamically handled and what messages are actually explicitly defined. By design the functionality of method_missing should handle any unknown message, but how do you know if you've broken something by adding a explicitly defined method?

The solution to this problem is to introduce an additional class that has the single responsibility of handling the dynamic method calls. In this case we have a class Recorder that handles recording unknown messages as well as playing back the messages or printing them. To reduce complexity we will introduce the MessageCollector class that handles the method_missing calls.

class MessageCollector
instance_methods.each do |meth|
undef_method meth unless meth =~ /^(__|inspect)/
end

def messages
@messages ||= []
end

def method_missing(sym, *args)
messages << [sym, args]
self
end
end

The record method of Recorder will create a new instance of the MessageCollector class and each additional chained call will be recorded. The play back and printing capabilities will remain on the Recorder object.

class Recorder
def play_for(obj)
@message_collector.messages.inject(obj) do |result, message|
result.send message.first, *message.last
end
end

def record
@message_collector ||= MessageCollector.new
end

def to_s
@message_collector.messages.inject([]) do |result, message|
result << "#{message.first}(args: #{message.last.inspect})"
end.join(".")
end
end

Labels: , ,




Thursday, February 28, 2008

Ruby: Replace method_missing with dynamic method definitions

You have methods you want to handle dynamically without the pain of debugging method_missing.

class Decorator
def initialize(subject)
@subject = subject
end

def method_missing(sym, *args, &block)
@subject.send sym, *args, &block
end
end

becomes

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

Motivation

Debugging classes that use method_missing can often be painful. At best you often get a NoMethodError on an object that you didn't expect, and at worst you get stack level too deep (SystemStackError).

There are times that method_missing is required. If the usage of an object is unknown, but must support unexpected method calls you may not be able to avoid the use of method_missing.

However, often you know how an object will be used and using Dynamically Define Method you can achieve the same behavior without relying on method_missing.

MechanicsExample: Dynamic delegation without method_missing

Delegation is a common task while developing software. Delegation can be handled explicitly by defining methods yourself or by utilizing something from the Ruby Standard Library such as Forwardable. Using these techniques gives you control over what methods you want to delegate to the subject object; however, sometimes you want to delegate all methods without specifying them. Ruby's Standard Library also provides this capability with the delegate library, but we'll assume we need to implement our own for this example.

The simple way to handle delegation (ignoring the fact that you would want to undefine all the standard methods a class gets by default) is to use method_missing to pass any method calls straight to the subject.

class Decorator
def initialize(subject)
@subject = subject
end

def method_missing(sym, *args, &block)
@subject.send sym, *args, &block
end
end

This solution does work, but it can be problematic when mistakes are made. For example, calling a method that does not exist on the subject will result in the subject raising a NoMethodError. Since the method call is being called on the decorator, but the subject is raising the error it may be painful to track down where the problem resides.

The wrong object raising a NoMethodError is significantly better than the dreaded stack level too deep (SystemStackError). This can be caused by something as simple as forgetting to use the subject instance variable and trying to use a non-existent subject method or any misspelled method. When this happens the only feedback you have is that something went wrong, but Ruby isn't sure exactly what it was.

These problems can be avoided entirely by using the available data to dynamically define methods at run time. The following example defines an instance method on the decorator for each public method of the subject.

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

Using this technique any invalid method calls will be correctly reported as NoMethodErrors on the decorator. Additionally, there's no method_missing definition, which should help avoid the stack level too deep problem entirely.

Example: Using user defined data to define methods

Often you can use the information from a class definition to define methods instead of relying on method_missing. For example, the following code relies on method_missing to determine if any of the attributes are nil.

class Person
attr_accessor :name, :age

def method_missing(sym, *args, &block)
empty?(sym.to_s.sub(/^empty_/,"").chomp("?"))
end

def empty?(sym)
self.send(sym).nil?
end
end

The code works, but it suffers from the same debugging issues that the previous example does. Utilizing Dynamically Define Method the issue can be avoided by defining the attributes and creating the empty_attribute? methods at the same time.

class Person
def self.attrs_with_empty_methods(*args)
attr_accessor *args

args.each do |attribute|
define_method "empty_#{attribute}?" do
self.send(attribute).nil?
end
end
end

attrs_with_empty_methods :name, :age
end

Labels: , , , ,




Thursday, December 20, 2007

Avoiding costly typos

Typos are generally unfortunate, but greatly upsetting when they cost you a few hours of your life. I have a few rules I try to follow in an attempt to conserve those valuable hours in the future.

Ruby has symbols. I love symbols. But, using symbols for comparison is an easy way for a typo to cost you time.

# example 1
name = :shane
name == :shane # => true
name == :chane # => false

# example 2
class Name
def self.shane
:shane
end
end

name = Name.shane
name == Name.shane # => true
name == Name.chane # => undefined method `chane' for Name:Class (NoMethodError)

Above, in example one, a typo simply returns false. However, in example two the typo gives me the immediate feedback that I made a mistake. You could argue that I should simply use a constant. Elephant case (All upper case) words bother me for some reason, but if you prefer constants that's cool too, you'll get the same benefit.

The method_missing method is dynamite. Used appropriately it's a powerful tool. However, there are often times when you simply don't need dynamite.

# example 3
class State < Struct.new(:state)
def method_missing(sym, *args)
sym.to_s.delete("?") == self.state
end
end

State.new("ready").ready? # => true
State.new("ready").reddy? # => false

# example 4
class State < Struct.new(:state)
def method_missing(sym, *args)
sym.to_s.delete("?") == self.stat
end
end

State.new("ready").ready? # ~> -:12:in `method_missing': stack level too deep (SystemStackError)

# example 5
class State < Struct.new(:state)
[:ready, :running, :finished].each do |element|
define_method :"#{element}?" do
self.state == element.to_s
end
end
end

State.new("ready").ready? # => true
State.new("ready").reddy? # ~> -:10: undefined method `reddy?' for #<struct State state="ready"> (NoMethodError)


Examples three and four illustrate the two common typos that can cost you time while utilizing method_missing. Example five shows an alternative that requires slightly more code, but is significantly better at letting you know when you've made a mistake.

If right now you are thinking "that's nice, but I write tests so I'll catch it there" then you get points for writing tests, but you missed one important note: If the typo is in your test you could be getting a false positive. I once found a bug where the same typo existed in a class and the test for the class. The result was broken production code and a green test suite.

The last tip builds on the first two: Don't use strings for comparison. As an alternative to using constants or class methods, you could define methods on a string (or a symbol) to query for the value.

RAILS_ENV = "development"
class << RAILS_ENV
["development", "test", "production"].each do |environment|
define_method :"#{environment}?" do
self == environment
end
end
end

RAILS_ENV.test? # => false
RAILS_ENV.development? # => true
RAILS_ENV.developmant? # ~> -:12: undefined method `developmant?' for "development":String (NoMethodError)

The last example is nice because it allows you to type less when doing a comparison and provides you better feedback if you do make a typo. (drop a +1 on this ticket if you want this feature in Rails core: http://dev.rubyonrails.org/ticket/10583)

Labels: , ,




Saturday, May 19, 2007

Ruby: method_missing alternatives

I love the value that method_missing provides; unfortunately, sometimes I get into trouble when I abuse the power Matz has bestowed upon me. If you've done much work with method_missing the following error message probably isn't foreign to you.
SystemStackError: stack level too deep
It's a painful message to see, as it's always problematic to debug. Given the inherent difficultly of debugging method_missing calls I thought it might be valuable to offer a few alternatives.

As an example I'll use the valid_for_[a group] methods that Validatable exposes. The Validatable validations allow you to put your validations into groups.
class User
include Validatable

attr_accessor :first_name, :last_name, :ssn
validates_presence_of :first_name, :last_name, :ssn, :groups => :account_creation
validates_presence_of :ssn, :groups => :persistence
end
Given the above User class you can expect the following behavior.
irb(main):010:0> user = User.new
=> #<User:0x696900>
irb(main):011:0> user.ssn = 111223333
=> 111223333
irb(main):012:0> user.valid_for_persistence?
=> true
irb(main):013:0> user.valid_for_account_creation?
=> false
irb(main):014:0> user.first_name = 'Shane'
=> "Shane"
irb(main):015:0> user.last_name = 'Harvie'
=> "Harvie"
irb(main):016:0> user.valid_for_persistence?
=> true
irb(main):017:0> user.valid_for_account_creation?
=> true
As you can see the User instance exposes the valid_for_account_creation? and the valid_for_persistence? methods. Of course, we want the valid_for_[a group] methods to change based on the groups defined in each class. Additionally, we only want valid_for_something? to be defined on a class if that class contains any validations in the something group. This looks like a classic situation for using method_missing; however, I chose one of the following other solutions.

Delegate Responsibility:
The basic idea behind this solution alternative is to move the method_missing logic into a class whose sole responsibility is the method_missing logic. For example, had I chosen this solution for Validatable I would have made the valid methods work like the following example.
irb(main):010:0> user = User.new
=> #<User:0x696900>
irb(main):011:0> user.ssn = 111223333
=> 111223333
irb(main):012:0> user.valid_for.persistence?
=> true
irb(main):013:0> user.valid_for.account_creation?
=> false
Given the above syntax I could have created a ValidFor class that encapsulated the logic for dynamically executing a subset of validations. A few cons for this solution are the obvious violation of the Law of Demeter and the fact that the stack overflow can still exist in the delegate class. However, I like to use this solution when the methods that are necessary are truly dynamic and cannot be easily predicted (i.e. the find_by_* methods that ActiveRecord::Base exposes). Additionally, the added class removes variables when trying to figure out where the logic went wrong.

Metaprogramming:
I chose this solution because I was able to easily identify which methods were necessary and define then at interpretation time. In fact the implementation for defining the valid methods is quite simple.
def create_valid_method_for_groups(groups) #:nodoc:
groups.each do |group|
self.class_eval do
define_method "valid_for_#{group}?".to_sym do
valid_for_group?(group)
end
end
end
end
Given the above code a class will define a new valid_for_[a group] method for each group given to any of it's validations. By explicitly defining each method I get much more manageable error messages when things do go wrong.

Labels: , ,




This page is powered by Blogger. Isn't yours?