Math::PI * (@wheel_diameter + @tire_diameter)
end
end
@tire_diameter * TIRE_WIDTH_FACTOR + @front_fork_travel * FRONT_SUSPENSION_FACTOR
end
end
@tire_diameter * TIRE_WIDTH_FACTOR
end
end
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)
Math::PI * (@wheel_diameter + @tire_width)
end
end
@tire_width * TIRE_WIDTH_FACTOR + @front_fork.travel * FRONT_SUSPENSION_FACTOR
end
end
@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)
...
@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.
@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:
Good post. Going to play with this some more. Can't wait for your Refactoring book in July 2009.
Here's another implementation you may be interested in http://github.com/dcadenas/state_pattern
Post a Comment