`
jlaky
  • 浏览: 41972 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Design Patterns in Ruby [Digest 9] Decorator

阅读更多

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 NumberingCheckSummedWriter < NumberingWriter
class TimestampedNumberingWriter < TimestampedWriter
class CheckSummedWriterLineNumberingWriter < CheckSummedWriter 

If you need a more complex writer with three feature, then it will be a mess.

A better solution would allow you to assemble the combination of features that you
really need:
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
 
Then  decorator will be:
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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics