The Decorator pattern is used when you need to add some feature to a class with wrapper rather than write a new method.
Think about the writer support chechsum write, line number write:
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
then when we write the plain text:
writer = EnhancedWriter.new('out.txt')
writer.write_line("A plain line")
when we write the checksum text:
writer.checksumming_write_line('A line with checksum')
puts("Checksum is #{writer.check_sum}")
when we write 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.
So we can use inheritant way to solve the problem:
class EnhancedWriter
class NumberingWriter < EnhancedWriter
class TimestampedWriter < EnhancedWriter
class CheckSummedWriter < EnhancedWriter
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
The client need not to worry about it is SimpleWriter or NumberingWriter instance:
writer = NumberingWriter.new(SimpleWriter.new('final.txt'))
writer.write_line('Hello out there')
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
The featured object we need can composed by:
writer = CheckSummingWriter.new(TimeStampingWriter.new(
NumberingWriter.new(SimpleWriter.new('final.txt'))))
writer.write_line('Hello out there')
The WriterDecorator is just a delegator class, we can use forwardable module
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
A Ruby dynamic solution of decorator problem is alias method:
w = SimpleWriter.new('out')
class << w
alias old_write_line write_line
def write_line(line)
old_write_line("#{Time.new}: #{line}")
end
end
This kind of method is common in Rails source code.
And the Ruby mix-in technique and dynamic extend method can also solve the problem:
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
class Writer
define write(line)
@f.write(line)
end
end
w = SimpleWriter.new('out')
w.extend(NumberingWriter)
w.extend(TimeStampingWriter)
w.write_line('hello')
The calling sequence will be TimeStampingWriter => NumberingWriter => SimpleWriter
The good example of method alias decorator is alias_method_chain