Use Modules, not Inheritence to share behavior in Ruby

Posted by Brian in Howto, Rails, snacks (August 10th, 2009)

When working in Rails, I often find that several classes have the same behavior. A common approach to share code between objects is the tried-and-true method of inheritance, wherein you create a base class and then extend your subclasses from the parent. The child classes get the behaviors from the inheritance. Basically, Object Oriented Programming 101.

Languages like Java and Ruby only let you inherit from one base class, though, so that isn’t gonna work. However, Ruby programmers can use modules to “mix in” behavior.

Mixing it up and Mixing it in

Let’s say we have a Person class.

class Person
  def sleep
    puts "zzz"
  end
end

Now let’s say we need to define a Ninja. We could create a Ninja class and inherit from Person, but that’s not really practical. Ruby developers really don’t like changing the type of a class just because its behavior changes.

Instead, let’s create a Ninja module, and put the behaviors in that module.

module Ninja
  def attack
    "You are dead."
  end
end

We can then use this multiple ways.

Mixing it in to the parent class

In a situation where we would like every Person to be a ninja, we simply use the include directive:

  class Person
    include Ninja
  end

Every instance of the class now has the Ninja behaviors. When we include a module, we are mixing in its methods to the instance. We could also use the extend method, but this mixes the methods in as class methods.

Notice here that we redefined the class definition. This appends to the existing class; it does not overwrite it. This can be a dangerous technique if used improperly because it can be difficult to track down. This is the simplest form of monkeypatching.

There’s an alternative method.

Mixing in to an instance

We can simply alter the instance of an object, applying these behaviors to a specific instance and affecting nothing else.

  @brian = Person.new
  @brian.extend Ninja

Here, we use the extend method because we need to apply these as class methods to the object instance. In Ruby, classes are objects themselves. Our instance needs to be extended.

A more practical exaple – Geolocation

I was working on a project this weekend where I had several models that needed latitude and longitude data pulled in via Google’s web service. I used the GeoKit gem and the Geokit-Rails plugins to make this happen, but I soon noticed I was adding the same code to multiple classes.

acts_as_mappable
after_validation_on_create :geocode_address

def geocode_address
  geo=Geokit::Geocoders::MultiGeocoder.geocode (address)
  errors.add(:address, "Could not Geocode address") if !geo.success
  self.lat, self.lng = geo.lat,geo.lng if geo.success
end

It seemed immediately apparent that this should go in a module which could be included into my classes. However, I wanted to also make the two class method calls – the after_validation_on_create callback and the acts_as_mappable macro.

Ruby has a facility for that type of situation

self included

def self.included(base)
#
end

This method is called when a module is included into a class, and it gives you access to the class that’s doing the including. You can use this as a handle to call any class methods. With that, my geolocation module looks like this:

module Geolocation
  
  def self.included(base)
    base.acts_as_mappable
    base.after_validation_on_create :geocode_address
  end

  def geocode_address
    geo=Geokit::Geocoders::MultiGeocoder.geocode (address)
    errors.add(:address, "Could not Geocode address") if !geo.success
    self.lat, self.lng = geo.lat,geo.lng if geo.success
  end
  
end

So now any class that needs geolocation just needs to look like this:

class Business
  include Geolocation
end

class Nonprofit
  include Geolocation
end

Summary

The above problem could have been solved by using a parent class like MappableOjbect that included the code, but it then makes putting in additional behavior more difficult. Using modules to share code is the preferred way to attach behaviors to objects in your applications.

Leave a reply

:mrgreen: :neutral: :twisted: :shock: :smile: :???: :cool: :evil: :grin: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: