Wednesday, November 19, 2008

State Pattern using module extension

I try to favour delegation over inheritance. But sometimes Replace Inheritance with Delegation can be difficult. It turns out that when you have an inheritance heirarchy, and state is used in both the superclass and subclasses, it can be difficult to remove the inheritance heirarchy and replace it with delegation. Let's look at an example. Here we're modeling bikes:

class Bicycle
def wheel_circumference
Math::PI * (@wheel_diameter + @tire_diameter)
end
end

class FrontSuspensionMountainBike < Bicycle
def off_road_ability
@tire_diameter * TIRE_WIDTH_FACTOR + @front_fork_travel * FRONT_SUSPENSION_FACTOR
end
end

class RigidMountainBike < Bicycle
def off_road_ability
@tire_diameter * TIRE_WIDTH_FACTOR
end
end

class RoadBike < Bicycle
def off_road_ability
raise "You can't take a road bike off-road"
end
end

In this heirarchy the @tire_diameter instance variable is used in both the superclass and subclass. You can imagine that if we were to try to have the Bicycle class delegate to a FrontSuspensionMountainBike object, we'd have to duplicate the @tire_diameter state. This becomes a bit awkward, particularly if @tire_diameter can change - you'd have to ensure that the the @tire_diameter in Bicycle is kept in synch with the one in FrontSuspensionMountainBike. I'd probably decide that it wasn't worth the effort, and keep the inheritance heirarchy.

But what if we wanted to change the type of bike at run-time? Perhaps we want to upgrade a RigidMountainBike (a bike with no suspension) to a FrontSuspensionMountainBike. Using the state pattern would be ideal, but the traditional state pattern uses delegation. With ruby modules we have a different option. Rather than represent the FrontSuspensionMountainBike and RigidMountainBike behaviour as subclasses of Bicycle, we could make them modules and extend Bicycle with the appropriate module for the behviour that we want:

mountain_bike = RigidMountainBike.new
front_suspension_bike = FrontSuspensionMountainBike.new

becomes

mountain_bike = Bicycle.new.extend(RigidMountainBike)
front_suspension_bike = Bicycle.new.extend(FrontSuspensionMountainBike)

class Bicycle
def wheel_circumference
Math::PI * (@wheel_diameter + @tire_width)
end
end

module FrontSuspensionMountainBike
def off_road_ability
@tire_width * TIRE_WIDTH_FACTOR + @front_fork.travel * FRONT_SUSPENSION_FACTOR
end
end

module RigidMountainBike
def off_road_ability
@tire_width * TIRE_WIDTH_FACTOR
end
end


So we could conceivably upgrade our mountain bike at run-time to add a fork with front suspension:

bike = Bicycle.new.extend(RigidMountainBike)
...
bike.add_front_suspension(fork)

module RigidMountainBike...
def add_front_suspension(fork)
@front_fork = fork
extend(FrontSuspensionMountainBike)
end
end

So we now have the ability to change behaviour at run-time, and we haven't introduced any duplication - the state and behaviour is still shared between the Bicycle class and the modules. But there's a problem:

bike.kind_of?(FrontSuspensionMountainBike) => true    
bike.kind_of?(RigidMountainBike) => true

We were able to mix in the FrontSuspensionMountainBike behaviour, but the RigidMountainBike behaviour still exists on the bike object. It turns out that ruby doesn't provide the ability to unmix a module. But all is not lost - there's an open-source library called mixology that does exactly what we want.
require 'mixology'

module RigidMountainBike

def add_front_suspension(fork)
@front_fork = fork
unmix(RigidMountainBike)
mixin(FrontSuspensionMountainBike)
end

end

It provides two methods: unmix (for removing a module from an object) and mixin, for adding a module to an object. Our bug is now fixed:

bike = Bicycle.new.extend(RigidMountainBike)
...
bike.add_front_suspension(fork)

bike.kind_of?(RigidMountainBike) => false

2 comments:

Jason Meridth said...

Good post. Going to play with this some more. Can't wait for your Refactoring book in July 2009.

Daniel Cadenas said...

Here's another implementation you may be interested in http://github.com/dcadenas/state_pattern