Open viphat opened 6 years ago
The Decorator pattern is the last of the "one object stands in for another" patterns that we will consider in this book. The first was the Adapter pattern; it hides the fact that some object has the wrong interface by wrapping it with an object that has the right interface. The second was the Proxy pattern. A proxy also wraps another object, but not with the intent of changing the interface. Instead, the proxy has the same interface as the object that it is wrapping. The proxy isn’t there to translate; it is there to control. Proxies are good for tasks such as enforcing security, hiding the fact that an object really lives across the network, and delaying the creation of the real object until the last possible moment. And then we have the subject of this chapter, the decorator, which enables you to layer features on to a basic object.
“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”
The Decorator pattern allows us to add behavior to a given object without having to add that behavior to the class of the object. This gives us the ability to decorate objects with additional behavior for use within a specific context. A decorator provides an interface which conforms to the object being enclosed (decorated), as well as the additional, context-specific operations. A decorator might also add operations to an existing operation on the object as it forwards the request along to the enclosed object.
# The base component that we are
# choosing to decorate with new
# behavior is the Coffee class.
class Coffee
attr_accessor :price
# The Coffee class has a
# price method.
def price
2.50
end
end
# Our first decorator in this
# exaxmple of the Decorator pattern
# is a CoffeeWithCream class.
class CoffeeWithCream
# The decorator accepts a coffee
# object. The coffee object is the
# component that is to be 'enclosed'
# by this decorator.
def initialize(coffee)
@coffee = coffee
end
# We are adding additional behavior
# to the 'price' method of the coffee
# component by adding to it the price
# of coffee cream.
def price
@coffee.price += 0.50
end
end
class CoffeeWithSugar
# The decorator accepts a coffee
# object. The coffee object is the
# component that is to be 'enclosed'
# by this decorator.
def initialize(coffee)
@coffee = coffee
end
# We are adding additional behavior
# to the 'price' method of the coffee
# component by adding to it the price
# of sugar.
def price
@coffee.price += 0.25
end
# We are adding an additional operation
# which we are then able to call on the
# enclosed, decorated coffee object.
def induce_sugar_rush
# code to induce sugar rush goes here.
end
end
# Here, we create a new Coffee object,
# enclose it with a CoffeeWithCream decorator,
# then enclose it with a CoffeeWithSugar
# decorator.
coffee = Coffee.new
coffee = CoffeeWithCream.new(coffee)
coffee = CoffeeWithSugar.new(coffee)
# We can call price and get the combined price of
# the coffee, coffee sugar, and coffee cream.
# The final total: $3.25.
puts coffee.price # 3.25
# We can call the added operation 'induce_sugar_rush'.
# The method executes and returns the message
# "sugar rush induced.".
puts coffee.induce_sugar_rush # "sugar rush induced."
Nested decorators Decorators can be nested recursively, adding layers of additional functionality to a given object.
Temporary responsibilities The Decorator pattern is a great choice for situations in which you wish to give an object temporary, additional responsibilities (behavior). Those responsibilities can then be revoked.
Subclass explosion Sometimes, the desired behavior of a class is so vast as to require an unwieldy amount of subclasses. The Decorator pattern allows for a more flexible way of dealing with such varied behavior. With decorators, you are able to do away with myriad subclasses and compose unique objects through the use of layered decorators.
Composites vs Decorators A decorator and its enclosed component are not identical, and cannot be relied upon to be identical. A composite and a leaf within a composite tree can be treated as being identical. A decorator only has one component, whereas a composite tree has many components. The intent of a decorator is to change or add responsibilities to an object. The intent of a composite tree is to be used as a form of object aggregation.
Adapters vs Decorators A decorator adds to or changes an objects responsibilities. An adapter gives an object a completely new interface.
Decorator Pattern enables you to easily add an enhancement to an existing object. It also allows you to layer features atop one another so that you can construct objects that have exactly the right set of capabilities that you need for any give situation.
The wrong approach
There is only one thing wrong with this approach: everything
First, every client that uses
EnhancedWriter
will need to know whether it is writing out numbered, checksummed, or time-stamped text. And the clients do not need to know this just once, perhaps to set things up—no, they need to know it continuously, with every line of data that they write out. If a client gets things wrong just once—for example, if it usestimestamping_write_line
when it meant to usenumbering_write_line
or if it uses plain oldwrite_line
when it meant to usechecksumming_write_line
— then the name of the class,EnhancedIO
, is going to seem more than a little ironic.An only slightly less obvious problem with this "throw it all in one class" approach is, well, that everything is thrown together in a one class. There is all of the line numbering code sitting alongside the checksum code, which is nestled up against the time stamp code, and all of them are locked together in the same class, used or not, just looking for trouble.
A better approach with decorators
To get our lines numbered, we just encase our
SimpleWriter
in aNumberingWriter
:Add more decorator
The ConcreteComponent is the "real" object, the object that implements the basic component functionality. In the writer example, the SimpleWriter is the ConcreteComponent. The Decorator class has a reference to a Component—the next Component in the decorator chain—and it implements all of the methods of the Component type. Our example has three different Decorator classes: one for line numbering, one for checksumming, and one for time stamping. Each Decorator layers its own special magic onto the workings of the base component, adding its own talent to at least one of the methods. Decorators can also add new methods—that is, operations that are not defined in the Component interface—although this behavior is optional. In our example, only the decorator that computes the checksum adds a new method.
Easing the Delegation Blues
We can see in the
WriterDecorator
class, which consists almost entirely of boilerplate methods that do nothing except delegate to the next writer down the line.We could eliminate all of this boring code with a variation on the
method_missing
technique, but theforwardable
module is probably a better fit. Theforwardable
module will automatically generate all of those dull delegating methods for us with very little effort.Decorating with modules
The last module added will be the first one called. Thus, in the preceding example, the processing would run from client to
TimeStampingWriter
toNumberingWriter
toWriter
.Wrapping Up
The Decorator pattern is a straightforward technique that you can use to assemble exactly the functionality that you need at runtime. It offers an alternative to creating a monolithic "kitchen sink" object that supports every possible feature or a whole forest of classes and subclasses to cover every possible combination of features. Instead, with the Decorator pattern, you create one class that covers the basic functionality and a set of decorators to go with it. Each decorator supports the same core interface, but adds its own twist on that interface. The key implementation idea of the Decorator pattern is that the decorators are essentially shells: Each takes in a method call, adds its own special twist, and passes the call on to the next component in line. That next component may be another decorator, which adds yet another twist, or it may be the final, real object, which actually completes the basic request.