Chapter 11. Improving Your Objects with a Decorator

Among the most basic questions of software engineering is this: How do you add features to your program without turning the whole thing into a huge, unmanageable mess? So far, you have seen how to split the internal workings of your objects up among a family of classes with the Template Method pattern and how to use the Strategy pattern to split off whole chunks of algorithms. You have also seen how to react to requests coming into your objects with the Command pattern and how to keep up with changes made to other objects with the Observer pattern. Composites and iterators each help in their own way in dealing with collections of objects.

But what if you simply need to vary the responsibilities of an object? What do you do when sometimes your object needs to do a little more, but sometimes a little less? In this chapter we will look at the Decorator pattern, which enables you to easily add an enhancement to an existing object. The Decorator pattern 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 given situation. As usual, we will take a look at a very Ruby-flavored alternative to the Decorator pattern. Finally, we will see why highly decorated objects are not always everyone's idea of a hero.

Decorators: The Cure for Ugly Code

Imagine that you have some text that needs to be written to a file. Sounds simple enough, but in your system sometimes you want to write out just the plain, unadorned text, while at other times you want to number each line as it gets written out. Sometimes you want to add a time stamp to each line as it goes out into the file. Sometimes you need a checksum from the text so that later on you can ensure that it was written and stored properly.

To handle these demands, you might just start out with an object that wraps a Ruby IO object and has several methods, one for each output variation:


   class EnhancedWriter
     attr_reader :check_sum
  
     def initialize(path)
       @file = File.open(path, "w")
       @check_sum = 0
       @line_number = 1
     end
  
     def write_line(line)
       @file.print(line)
       @file.print(" ")
     end
  
     def checksumming_write_line(data)
       data.each_byte {|byte| @check_sum = (@check_sum + byte) % 256 }
       @check_sum += " "[0] % 256
       write_line(data)
     end
  
     def timestamping_write_line(data)
       write_line("#{Time.new}: #{data}")
     end
  
     def numbering_write_line(data)
       write_line("%{@line_number}: #{data}")
       @line_number += 1
     end
  
     def close
       @file.close
     end
   end


You can then use EnhancedWriter to write out ordinary text:


   writer = EnhancedWriter.new('out.txt')
   writer.write_line("A plain line")


Or a line that gets included in the checksum:


   writer.checksumming_write_line('A line with checksum')
   puts("Checksum is #{writer.check_sum}")


Or a time-stamped line or a numbered one:


   writer.timestamping_write_line('with time stamp')
   writer.numbering_write_line('with line number')


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 uses timestamping_write_line when it meant to use numbering_write_line or if it uses plain old write_line when it meant to use checksumming_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.

You might be able to separate out all of these text writing concerns by creating a base class and subclasses—in other words, use our old friend inheritance—along the lines shown in the UML diagram in Figure 11-1.

Figure 11-1. Solving the enhanced writer problem with inheritance

image

But what if you want a checksum of your numbered output? What if you want to put line numbers on your output, but only after you add time stamps to it? You can still do it, but the number of classes does seem to be getting out of hand, as is painfully clear in Figure 11-2.

Figure 11-2. Out-of-control inheritance

image

Now consider that even with the forest of classes shown in Figure 11-2, we still can’t get a checksum of that time-stamped text after we have line-numbered it. The trouble is that the inheritance-based approach requires you to come up with all possible combinations of features up-front, at design time. Chances are, you are not really going to need every single combination—you are just going to need the combinations that you need.

A better solution would allow you to assemble the combination of features that you really need, dynamically, at runtime. Let’s start over with a very dumb object that just knows how to write the plain, unadorned text and do a few other file-related operations:


   class SimpleWriter
     def initialize(path)
       @file = File.open(path, 'w')
     end
  
     def write_line(line)
       @file.print(line)
       @file.print(" ")
     end
  
     def pos
       @file.pos
     end
  
     def rewind
       @file.rewind
     end
  
     def close
       @file.close
     end
   end


If you want your lines numbered, insert an object (perhaps one called NumberingWriter) between your SimpleWriter and the client, an object that adds a number to each line and forwards the whole thing on to the basic SimpleWriter, which then writes it to disk. NumberingWriter adds its own contribution to the abilities of SimpleWriter—in a sense, it decorates SimpleWriter; hence the name of the pattern. We plan to write a bunch of these decorator objects, so let’s factor out the generic code into a common base class:


   class WriterDecorator
     def initialize(real_writer)
       @real_writer = real_writer
     end
  
     def write_line(line)
       @real_writer.write_line(line)
     end
  
     def pos
       @real_writer.pos
     end
  
     def rewind
       @real_writer.rewind
     end
  
     def close
       @real_writer.close
     end
   end
  
   class NumberingWriter < WriterDecorator
     def initialize(real_writer)
       super(real_writer)
       @line_number = 1
     end
  
     def write_line(line)
       @real_writer.write_line("#{@line_number}: #{line}")
       @line_number += 1
     end
   end


Because the NumberingWriter class presents the same core interface as the plain old writer, the client does not really have to worry about the fact that it is talking to a NumberingWriter instead of a plain old SimpleWriter. At their most basic, both flavors of writer look exactly the same.

To get our lines numbered, we just encase our SimpleWriter in a NumberingWriter:


   writer = NumberingWriter.new(SimpleWriter.new('final.txt'))
   writer.write_line('Hello out there')


We can follow the same pattern to build a decorator that computes checksums. That is, another object will sit between the client and the SimpleWriter, this time summing up all of the bytes before it sends them off to the SimpleWriter for writing:


   class CheckSummingWriter < WriterDecorator
     attr_reader :check_sum
  
     def initialize(real_writer)
       @real_writer = real_writer
       @check_sum = 0
     end
  
     def write_line(line)
       line.each_byte {|byte| @check_sum = (@check_sum + byte) % 256 }
       @check_sum += " "[0] % 256
       @real_writer.write_line(line)
     end
   end


The CheckSummingWriter is a little different from our first decorator in that it has an enhanced interface. In addition to the usual methods found on all of the writers, CheckSummingWriter sports the check_sum method.[1]

Finally, we can write a class that adds time stamps to the data as it goes by:


   class TimeStampingWriter < WriterDecorator
     def write_line(line)
       @real_writer.write_line("#{Time.new}: #{line}")
     end
   end


Now here is the punchline: Because all of the decorator objects support the same basic interface as the original, the "real" object that we supply to any one of the decorators does not actually have to be an instance of SimpleWriter—it can, in fact, be any other decorator. This means that we can build arbitrarily long chains of decorators, with each one adding its own secret ingredient to the whole. We can, for example, finally get that checksum of that time-stamped text, after we have line-numbered it:


   writer = CheckSummingWriter.new(TimeStampingWriter.new(
                NumberingWriter.new(SimpleWriter.new('final.txt'))))
  
   writer.write_line('Hello out there')


Formal Decoration

All of the players in the Decorator pattern, as shown in Figure 11-3, implement the component interface.

Figure 11-3. The Decorator pattern

image

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

The Decorator pattern takes one bit of GoF advice to heart: It incorporates a lot of delegation. We can see this 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 that we learned in Chapter 10, but the forwardable module is probably a better fit. The forwardable module will automatically generate all of those dull delegating methods for us with very little effort. Here is our WriterDecorator class rewritten to take advantage of forwardable:


   require 'forwardable'
  
   class WriterDecorator
     extend Forwardable
  
     def_delegators :@real_writer, :write_line, :rewind, :pos, :close
  
     def initialize(real_writer)
      @real_writer = real_writer
     end
  
   end


The forwardable module supplies the def_delegators class method,[2] which takes two or more arguments. The first argument is the name of an instance attribute.[3] It is followed by the name of one or more methods. The def_delegators method will add all of the named methods to your class, and each of those new methods in turn delegates to the object referred to by the attribute. Thus the WriterDecorator class will end up with write_line, rewind, pos, and close methods, all of which delegate to @real_writer.

The forwardable module is more of a precision weapon than the method_missing technique. With forwardable, you have control over which methods you delegate. Although you could certainly put logic in method_missing to pick and choose which methods to delegate, the method_missing technique really shines when you want to delegate large numbers of calls.

Dynamic Alternatives to the Decorator Pattern

The runtime flexibility of Ruby presents some interesting alternatives to the GoF Decorator pattern. In particular, we can obtain most of that decorator goodness either by dynamically wrapping methods or via module decorations.

Wrapping Methods

We have already seen that Ruby allows us to modify the behavior of single instances or whole classes pretty much anytime. Armed with this flexibility, plus some knowledge of the alias keyword, we can turn a plain-vanilla writer into a time-stamping writer:


   w = SimpleWriter.new('out')
  
   class << w
  
     alias old_write_line write_line
  
     def write_line(line)
       old_write_line("#{Time.new}: #{line}")
     end
  
   end


The alias keyword creates a new name for an existing method. In the preceding code, we start by creating an alias for the original write_line method, so that we can refer to it as either write_line or old_write_line. Then we redefine the write_line method, but—critically—old_write_line continues to point to the original definition. It's all downhill from there: The new method time-stamps each line and then calls the original method (now known only as old_write_line) to write the time-stamped text out.

Luckily for all you would-be decorators, the "wrap the method" technique is a bit limited. It suffers from the danger of method name collisions. For example, as our code stands right now, if we tried to add two sets of line numbers to our output, we would lose our reference to the original write method because we do the alias twice. You could probably come up with a clever scheme to avoid name collisions, but as your decorations become more complicated they just cry out to live in their own classes. Nevertheless, for smaller-scale problems, the method-wrapping technique is useful enough that it should be in every Ruby programmer's toolkit.

Decorating with Modules

Another way to add capabilities to a Ruby object is to dynamically mix in modules with the extend method. To use this technique, we need to refactor our decorating classes into modules:


   module TimeStampingWriter
     def write_line(line)
       super("#{Time.new}: #{line}")
     end
   end
  
   module NumberingWriter
     attr_reader :line_number
  
     def write_line(line)
       @line_number = 1 unless @line_number
       super("#{@line_number}: #{line}")
       @line_number += 1
     end
   end


The extend method essentially inserts a module into an object's inheritance tree before its regular class. We can, therefore, start with an ordinary writer and then simply add in the functionality that we need:


   w = SimpleWriter.new('out')
   w.extend(NumberingWriter)
   w.extend(TimeStampingWriter)
  
   w.write_line('hello')


The last module added will be the first one called. Thus, in the preceding example, the processing would run from client to TimeStampingWriter to NumberingWriter to Writer.

While either of the dynamic techniques work—and they are, in fact, the runaway choice in existing Ruby code—they do have one disadvantage: With both of these techniques, it is hard to undo the decoration. Unwrapping an aliased method is likely to be tedious, and you simply cannot un-include a module.

Using and Abusing the Decorator Pattern

The classic Decorator pattern is loved more by the folks who build the thing than by those who use it. As we have seen, the Decorator pattern helps the person who is trying to build all of this functionality neatly separate out the various concerns—line numbering in this class, checksumming over in this other class, time stamping in a third. The irritating moment comes when someone tries to assemble all of these little building block classes into a working whole. Instead of being able to instantiate a single object, perhaps with EnhancedWriter.new(path), the client has to put all of the pieces together itself. Of course, there are things that the author of a decorator implementation can do to ease the assembly burden. If there are common chains of decorators that your clients will need, by all means provide a utility (perhaps a Builder?[4]) to get that assembly done.

One thing to keep in mind when implementing the Decorator pattern is that you need to keep the component interface simple. You want to avoid making the component interface overly complex, because a complex interface will make it that much harder to get each decorator right.

Another potential drawback of the Decorator pattern is the performance overhead associated with a long chain of decorators. When you trade in that single, monolithic ChecksummingNumberingTimestampingWriter class for a chain of decorators, you are gaining a lot of programming compartmentalization and code clarity. Of course, the price you pay is that you are multiplying the number of objects floating around in your program. This may not be much of a concern if, as in our writer example, you are dealing with a handful of open files. It becomes much more problematic if we are talking about every employee in a very large company. Remember, too, that besides the number of objects involved, any data that you send through a chain of N decorators will change hands N times as the first decorator hands it off to the second decorator, which hands it off to the third decorator, and so on.

Finally, one drawback of the method-aliasing technique for decorating objects is that it tends to make your code harder to debug. Think about it: Your methods will show up in the stack trace with different names than they have in the code stored in your source files. This is not a fatal difficulty, just one more thing to keep in mind.

Decorators in the Wild

A good example of the method-aliasing style of decorating objects can be found ActiveSupport, the package of support utilities used by Rails. ActiveSupport adds a method to all objects called alias_method_chain. The alias_method_chain method allows you to decorate your methods with any number of features. To use alias_method_chain, you start with a plain-vanilla method in your class, such as write_line:


   def write_line(line)
     puts(line)
   end


You then add in another method that adds some decoration to the original method:


   def write_line_with_timestamp(line)
     write_line_without_timestamp("#{Time.new}: #{line}")
   end


Finally, you call alias_method_chain:

      alias_method_chain :write_line, :timestamp

The alias_method_chain method will rename the original write_line method to write_line_without_timestamp and rename write_line_with_timestamp to plain old write_line, essentially creating a chain of methods. The nice thing about alias_method_chain is that, as its name suggests, you can chain together a number of enhancing methods. For example, we could add on a line-numbering method:


   def write_line_with_numbering(line)
     @number = 1 unless @number
     write_line_without_numbering("#{@number}: #{line}")
     @number += 1
   end
  
   alias_method_chain :write_line, :numbering


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.

The Decorator pattern lets you start with some basic functionality and layer on extra features, one decorator at a time. Because the Decorator pattern builds these layers at runtime, you are free to construct whatever combination you need, at runtime.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.46.141