Ruby Day 4 Forwardable26 Jun 2010 |
|
|---|---|
|
Today’s topic is a favourite of upper management: delegation. Delegation is when you make someone else do something for you, but you still get the credit for it. The Forwardable class in Ruby does much the same thing. It lets an object call methods on another object, but appearing to the user like the first object is actually doing the work. To see the advantage of forwardable, we can look at an example where it’s not used. We will create two classes, one called MarsExplorer and a second called MissionControl. We want to be able to control the mars robot in a more suitable, oxygen filled environment, which would be Mission Control. |
class MarsExplorer
def forward
@speed = 1
puts @speed
end
def backwards
@speed = -1
puts @speed
end
def stop
@speed = 0
puts @speed
end
end |
|
Our robot can move forwards, backwards, and it can stop. After each action we ask it to print out the speed, so we can keep track of when the methods are called. |
class MissionControl
def initialize
@robot = MarsExplorer.new
end
def forward
@robot.forward
end
def backwards
@robot.backwards
end
def stop
@robot.stop
end
end |
|
Phew, that was a lot of typing! Too much, in my opinion. For each method in MarsExplorer, we have had to write the same method definition in MissionControl. If we ever change our robot class to have a turn method, or we change ‘stop’ to ‘brake’, then we will have quite a few changes to make in our MissionControl class. In addition, MissionControl is now very cluttered with method definitions that aren’t directly related to the class. It would be better if we could simply let MissionControl respond to the methods of MarsExplorer, in a simple and unobtrusive way. We can achieve this by using Forwardable, as you can see in our updated MissionControl. |
require 'forwardable'
class MissionControl
extend Forwardable
def_delegators :@robot, :forward, :backwards, :stop
def_delegator :@robot, :stop, :brake
def initialize
@robot = MarsExplorer.new
end
end |
|
Notice that all of the method definitions are gone, with the exception of initialize, which hasn’t changed. Firstly, we have said that we want to make MissionControl extend Forwardable |
extend Forwardable |
|
This gives us access to the methods in Forwardable that follow, def_delegators and def_delegator. When we want to provide access to multiple methods, and we don’t mind calling them by the same name, we can use def_delegators. The syntax is explained below. |
|
|
def_delegators :theObject, :method1, :method2, :method3 |
def_delegators :@robot, :forward, :backwards, :stop |
|
We are saying that MissionControl should respond to the methods forward, backwards, and stop, and it should respond by calling the method of the same name on the robot object. Instead of 3 method definitions, we only need one line stating which object to delegate to, and what methods it should allow delegation of. If we want to change the apparent name of the method, we can use def_delegator. |
|
|
def_delegator :@theObject, :methodName, :desiredMethodName |
def_delegator :@robot, :stop, :brake |
|
Now we can call the method ‘brake’ from MissionControl which will call the ‘stop’ method on our MarsExplorer. Neat! Let’s take it for a spin. |
control = MissionControl.new
control.forward # 1
control.backwards # -1
control.stop # 0
control.brake # 0 |
|
Now, not all of us are engineers at NASA, so where might you use Forwardable in a real life? It is useful anywhere you find yourself writing methods similar to the ones in the first MissionControl implementation, simple wrapping of another object’s methods. It can also be used as a form of adapter. By defining your own class that delegates to another object, and only calling the methods on this class, the delegate object can be set to anything. For example, if you are writing an application that will be using a database, but you’re not sure if it will be MySQL, MSSQL, or PostgreSQL, you can create a wrapper class that has def_delegator set for each method you want to be able to call. Your implementation will use this wrapper class for all database access, with the method calls being delegated to whichever object you have set. If the underlying database object has different method names, you only need to update your def_delegator definitions, and all of your implementation code will remain the same. Here’s a contrived example to demonstrate this. |
class MyDatabase
extend Forwardabble
def_delegator :@db, :newMySQL, :new
def initialize
@db = MySQL.new
end
end
db = MyDatabase.new
db.new |
|
Suppose that there exists a ‘MySQL’ class, which provides the method ‘newMySQL’ to create a new database. If we build our implementation on the MyDatabase class instead of the MySQL class, we will be able to switch to PostgreSQL in the future by simply changing the def_delegator |
def_delegator :@db :newPostgreSQL, :new |
|
Our implementation code remains untouched, and we have only made one small change. While this example is entirely fictional, it shows how you can create a level of abstraction with Forwardable. |
|