Turning responsibility inside-out via delegation
What is the delegation pattern? You find this where you have an object that expresses a certain behaviour externally but internally defers, or, delegates the responsibility for implementation to another object in an inversion of responsibility.
Turning responsibility inside-out
Ruby provides five ways to accomplish this: three (SimpleDelegator, DelegateClass and Delegator) encapsulated in the delegate library and the remaining two (Forwardable and SingleForwardable) via the forwardable library.
Let's use a queue data structure that delegates to an array to illustrate the various ways of accomplishing delegation.
SimpleDelegator
This is the simplest way to accomplish delegation. You simply pass an object to the constructor and all methods supported by the object will be delegated.
_This object can be changed later._
require 'delegate'
class Queue
def initialize
@sd = SimpleDelegator.new([]) # we delegate to an array object
end
def enqueue(element)
@sd.push(element)
end
def dequeue
@sd.shift
end
end
q = Queue.new
q.enqueue(10) # [10]
q.enqueue(20) # [10, 20]
q.dequeue # [20]
If you want to change the object you're delegating to you just use __setobj__(obj). You should just keep in mind that this does *not* cause SimpleDelegator’s methods to change which means that you should only be delegating to objects of the same type as the original delegate to avoid nastiness.
DelegateClass
If SimpleDelegator does not spin your propeller then the next step would be to look at DelegateClass. Using the top level DelegateClass method to setup delegation through class inheritance is considered more flexible and is seemingly the most common use for this library.
require 'delegate'
class Queue < DelegateClass(Array) # we delegate to an array object
def initialize(arg=[])
super(arg)
end
alias_method :enqueue, :push # alias_method sets up the method aliasing for us
alias_method :dequeue, :shift
end
q = Queue.new
q.enqueue(10) # [10]
q.enqueue(20) # [10, 20]
q.dequeue # [20]
Delegator
The final tool from the delegator library is Delegator which provides you with full control over the delegation scheme. The contrived example below is derived from the SimpleDelegator’s implementation.
require 'delegate'
class QueueDelegator < Delegator # inherit from the Delegator class
def initialize(obj)
super # pass obj to Delegator constructor
@_sd_obj = obj # store obj for future use
end
def __getobj__
@_sd_obj # return the object we are delegating to
end
def __setobj__(obj)
@_sd_obj = obj # change delegation object, a feature we're providing
end
end
The conventional wisdom here however is that you should most likely be using the forwardable library instead of Delegator.
Fowardable
If you need class-level delegation this is your beast of burden.
require 'forwardable'
class Queue
extend Forwardable
def initialize(obj=[])
@queue = obj # delegate to this object
end
def_delegator :@queue, :push, :enqueue
def_delegator :@queue, :shift, :dequeue
def_delegators :@queue, :clear, :empty?, :length, :size, :<<
end
There are a few things to take note of here. First, def_delegator is used to set up the delegation relationship between the method call, the delegated object and the method to call on the delegated object.
Second, notice the syntax (:@queue, instead of @queue or :queue) to specify the delegated object we're defining methods for. This is simply an artefact of the way that Forwardable is implemented.
SingleForwardable
Where Forwardable provides class-level delegation, SingleForwardable provides object level delegation. For this example I'll simply copy the example provided in the library documentation.
require 'forwardable'
printer = String.new
printer.extend SingleForwardable # prepare object for delegation
printer.def_delegator "STDOUT", "puts" # add delegation for STDOUT.puts()
printer.puts "Howdy!"
Epilogue
Using DelegateClass and Forwardable for your delegation needs will most likely cover most of the cases you may end up needing to implement the delegator pattern.
No comments:
Post a Comment