Object-Oriented Design Libraries
One of the interesting things about Ruby is the way it blurs the distinction between design and implementation. Ideas that have to be expressed at the design level in other languages can be implemented directly in Ruby.
To help in this process, Ruby has support for some design-level strategies.
-
The Visitor pattern (Design Patterns, ) is a way of traversing a collection without having to know the internal organization of that collection.
-
Delegation is a way of composing classes more flexibly and dynamically than can be done using standard inheritance.
-
The Singleton pattern is a way of ensuring that only one instantiation of a particular class exists at a time.
-
The Observer pattern implements a protocol allowing one object to notify a set of interested objects when certain changes have occurred.
Normally, all four of these strategies require explicit code each time they're implemented. With Ruby, they can be abstracted into a library and reused freely and transparently.
Before we get into the proper library descriptions, let's get the simplest strategy out of the way.
The Visitor Pattern
It's the method
each
.
Object delegation is a way of
composing objects---extending an object with the capabilities of another---at runtime. This promotes writing flexible, decoupled code, as there are no compile-time dependencies between the users of the overall class and the delegates.
The Ruby
Delegator
class implements a simple but powerful delegation scheme, where requests are automatically forwarded from a master class to delegates or their ancestors, and where the delegate can be changed at runtime with a single method call.
The
delegate.rb
library provides two mechanisms for allowing an object to forward messages to a delegate.
- For simple cases where the class of the delegate is fixed, arrange for the master class to be a subclass of
DelegateClass
, passing the name of the class to be delegated as a parameter (Example 1). Then, in your class's initialize
method, you'd call the superclass, passing in the object to be delegated. For example, to declare a class Fred
that also supports all the methods in Flintstone
, you'd write
class Fred < DelegateClass(Flintstone)
def initialize
# ...
super(Flintstone.new(...))
end
# ...
end
|
This is subtly different from using subclassing. With subclassing, there is only one object, which has the methods and the defined class, its parent, and their ancestors. With delegation there are two objects, linked so that calls to one may be delegated to the other.
- For cases where the delegate needs to be dynamic, make the master class a subclass of
SimpleDelegator
(Example 2). You can also add delegation capabilities to an existing object using SimpleDelegator
(Example 3). In these cases, you can call the __setobj__
method in SimpleDelegator
to change the object being delegated at runtime.
Example 1. Use the
DelegateClass
method and subclass the result when you need a class with its own behavior that also delegates to an object of another class. In this example, we assume that the
@sizeInInches
array is large, so we want only one copy of it. We then define a class that accesses it, converting the values to feet.
require 'delegate'
sizeInInches = [ 10, 15, 22, 120 ]
class Feet < DelegateClass(Array)
def initialize(arr)
super(arr)
end
def [](*n)
val = super(*n)
case val.type
when Numeric; val/12.0
else; val.collect {|i| i/12.0}
end
end
end
|
sizeInFeet = Feet.new(sizeInInches) |
sizeInInches[0..3] |
|