Ruby Case Statements and `kind_of?`

You’re an object – Stand up straight and act like one!

Imagine you have this code:

Code 1:

class MyView
  attr_reader :target
  
  def initialize(target)
    @target = target
  end

  def double
    case target
      when Numeric  then target * 2 
      when String   then target.next    # lazy example fail
      when Array    then target.collect {|o| MyView.new(o).double}
      else
        raise “don’t know how to double #{target.class} #{target.inspect}”
    end
  end
end

It does just what you’d expect.

Output 1:

>> MyView.new(‘aaa’).double
=> "aab"
>> MyView.new(49).double
=> 98
>> MyView.new([‘b’, 6]).double
=> ["c", 12]
>> MyView.new({‘x’=>‘y’}).double
RuntimeError: don’t know how to double Hash {"x"=>"y"}
        from (irb):73:in `double'
        from (irb):80
        from :0

You’re probably familiar with this pattern. Its everywhere in Rails and you likely use it in your own code.

I want to say, in the nicest possible way, that this style of code is wrong, wrong, wrong and that you should do a different thing.

Okay, now that I have your attention, I’m not trying to start a fight. I’m not the best Ruby person around and I’m definitely not the best OO designer, but I do have an alternative pattern to suggest.

I’m aiming to start a discussion, not a religious war. Strong opinions are welcome.

What’s happening up there?

MyView needs to operate on several other objects. It knows:

  • the classes of all the objects that it can interact with, and
  • the behavior that should occur for each of those classes.

The case statement above is really an if statement that checks ‘kind_of?’ on each collaborating object.

I object to this code because:

  • use of kind_of? is a code smell that says your code is procedural, not object oriented, and
  • if you write procedural code your application will gradually become impossible to change and everyone will hate you.

Why is it wrong?

If I change how double works on any of these classes, MyView must change, but that’s not the real problem. What happens if MyView wants to double some new kind of object? I have to go into MyView and add a new branch to the case statement. How annoying is that?

But that’s the least of it. If I’m writing code that follows this pattern, I likely have many classes that do stuff based on the classes of their collaborators. My entire application behaves this way. Every time I add a new collaborating object I have to go change code everywhere. Each subsequent change makes things worse. My application is a teetering house of cards and eventually it will come tumbling down.

Also, what if someone else wants to use MyView with their new SuperDuper object? They can’t reuse MyView without changing it since MyView has a very limited notion of what kinds of objects can be doubled.

MyView is both rigid and closed for extension.

What should I have done instead?

Something like this.

Code 2:

class Numeric
  def double
    self * 2
  end
end

class String
  def double
    self.next 
  end
end

class Array
  def double
    collect {|e| e.double}
  end
end

class Object
  def double
    raise "don't know how to double #{self.class} #{self.inspect}"
  end
end

class MyView
  attr_reader :target
  
  def initialize(target)
    @target = target
  end

  def double
    target.double
  end
end

Using this new code, Output 1 will be the same as before, but now we can also:

Output 2:

>> 'aaa'.double
=> "aab"
>> 49.double
=> 98
>> ['b', 6].double
=> ["c", 12]

In this example, objects are what they are and because of that they behave the way they do.

That statement is deceptively simple but incredibly important. Objects are what they are so they do what they do.

It is not the job on any object to tell any other object how to behave. Objects create behavior by passing messages back and forth. They tell each other what, not how.

What is the event/notification/request and it is the responsibility of the sender.

How is the behavior/implementation and it should be completely hidden in the receiver.

Code 2 is object oriented because it relies on a network of interacting objects to produce behavior. Each object knows its own implementation and it exhibits that behavior when it receives a message.

No object in your system should have to know the class of any other object in order to know how to behave. Everything is a Duck. Tell the Duck what and the Duck should know how.

This way you can change how any object is doubled by changing its own specific implementation. More importantly, MyView can now collaborate with any kind of object, as long as the object implements double. This is a nice theory, but it seems impossible in practice.

Nah, it’s easy. But it means thinking about objects in a more Object Oriented way.

In order to write code like Code 2, you have to believe in objects.

In Ruby, everything is an object. I know that you know this, but I suspect that you don’t feel it in your bones. You came from those languages where you couldn’t change String so now you operate as if String (and Symbol and Array and …) are static types.

Throw off your shackles. Ruby is a network of interacting objects and you can add behavior to all of them. When you find yourself saying, if you’re this kind of thing, do this, but if you’re that kind of thing, do that, it’s your cue to push the behavior back into those individual objects.
Sound bites.

Always send messages if you can.
Implement behavior only when there’s no one left to ask.
The last object that could possibly receive the message is the object that should implement the behavior. Therefore:

  • MyView should not know how Numeric implements double.
  • MyView should just ask target to double itself.
  • All possible targets must implement double.
  • Object should implement double to help you get your Ducks in a row.

Lest you think this is all academic…

Here’s the code to use this Pattern in some of the form_for methods in ActionView::Helpers. Ponder the implications. Diff of the changes. actionpack/lib/action_view/helpers/form_helper.rb actionpack/lib/action_view/helpers/prototype_helper.rb actionpack/lib/action_view/helpers/form_helper_core_extensions.rb

Posted on June 12, 2009 .