viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

[Design Patterns in Ruby] - Chapter 11: Decorator Design Pattern #245

Open viphat opened 6 years ago

viphat commented 6 years ago

How do you add features to your program without turning the whole thing into a huge, unmanageable mess? 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 sometime a little less?

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

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.

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("\n")
 end

 def checksumming_write_line(data)
   data.each_byte {|byte| @check_sum = (@check_sum + byte) % 256 }
   @check_sum += "\n"[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
writer = EnhancedWriter.new('out.txt')
writer.write_line("A plain line")
writer.checksumming_write_line('A line with checksum')
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.

A better approach with decorators

class SimpleWriter
 def initialize(path)
   @file = File.open(path, 'w')
 end

 def write_line(line)
   @file.print(line)
   @file.print("\n")
 end

 def pos
   @file.pos
 end

 def rewind
   @file.rewind
 end

 def close
   @file.close
 end
end
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

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')

Add more decorator

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 += "\n"[0] % 256
   @real_writer.write_line(line)
 end
end

class TimeStampingWriter < WriterDecorator
 def write_line(line)
   @real_writer.write_line("#{Time.new}: #{line}")
 end
end
writer = CheckSummingWriter.new(TimeStampingWriter.new(
                NumberingWriter.new(SimpleWriter.new('final.txt'))))

writer.write_line('Hello out there')

ch11fig03

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 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.

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

Decorating with 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
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.

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.

viphat commented 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.

viphat commented 6 years ago

Source

“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.