1. ホーム
  2. スクリプト・コラム
  3. ルビートピックス

デザインパターンにおけるDecoratorパターンを用いたRubyの例

2022-01-31 15:32:09

概要
       オブジェクト指向の開発に携わったことがある人なら、継承を使ってクラスやオブジェクトに追加の動作を実装することは、すべてのオブジェクト指向言語の基本機能である。既存のクラスにいくつかのメソッドがない場合、またはメソッドにさらに機能(魅力)を追加する必要がある場合、そのクラスから継承して新しいクラスを生成することがあります - これは余分なコードの上に構築されるものです。
      既存のクラスを継承することで、親クラスのメソッドと合わせて子クラスに独自のメソッドを持たせることができます。しかし、そのようなメソッドは静的なものであり、いつどのように追加されるかはユーザー側でコントロールできません。すでに初期化されているオブジェクトの振る舞いを変えたい場合はどうすればよいのでしょうか?あるいは、多くのクラスの振る舞いを継承したい場合、代わりに何をすればいいのでしょうか?前者は実行時にしかできませんし、後者は当然可能ですが、たくさんの異なるクラスができることになりかねません - 恐ろしいことです。
問題点
      クラスの中に余計なコードを書かないで、基本的な機能やめったに使わない機能を簡単に追加できるように、どのようにコードを整理しているのでしょうか?

解決方法

  •        オブジェクトにいくつかの責任や動作を動的に追加します。Decoratorパターンは、サブクラスを生成するよりも柔軟に機能を追加することができます。
  •        サブクラスを変更する際の柔軟な解決策を提供します。Decoratorパターンは、元のクラスファイルを変更することなく、継承を使用してオブジェクトの機能を動的に拡張します。これは、実際のオブジェクトを包むラッパーオブジェクト(別名:デコレーション)を作成することで行います。
  •        デコレーター・パターンは、一連のサブクラスに対して使用するとより効果的です。親クラスから派生した)サブクラスのファミリーがあり、サブクラスから独立して使用するための機能を追加する必要がある場合、コードの重複や具象サブクラスの増加を避けるためにデコレーターパターンを使用することができます。

 適用性

       Decoratorパターンを使用するのは以下のようなケースです。

  •       他のオブジェクトに影響を与えることなく、動的かつ透過的な方法で、単一のオブジェクトに責任を追加します。
  •       取り消すことができる責任を処理します。
  •       サブクラスを生成する方法で拡張ができない場合。一つの状況として、以下のような独立した拡張子が多数存在する場合がある。

      それぞれの組み合わせに対応するために大量のサブクラスが生成され、サブクラスの数が爆発的に増えることになる。

      もう一つのシナリオは、クラス定義が隠されている、またはクラス定義を使用してサブクラスを生成できないことです。
インスタンス

class SimpleWriter 
 def initialize(path) 
  @file = File.open(path,"w") 
 end 
  
 def write_line(line) 
  @file.print(line) 
  @file.print("\n") 
 end 
  
 #characters 
 def pos 
  @file.pos 
 end 
  
 # It will point the file pointer to the beginning of the file 
 def rewind 
  @file.rewind 
 end 
  
 def close 
  @file.colse 
 end 
end 
  
sw = SimpleWriter.new("test.txt") 
sw.write_line("hello") 
puts sw.pos 
puts sw.rewind 
  
#Base class 
class WriterDecorator 
 def initialize(real_writer) 
  @real_writer = real_writer 
 end 
  
 def write_line 
  @real_writer.write_line 
 end 
  
 def pos 
  @real_writer.pos 
 end 
  
 def rewind 
  @real_writer.rewind 
 end 
  
 def close 
  @real_writer.close 
 end 
end 
  
class NumberingWriter < WriterDecorator 
 attr :line_number 
 def initialize(real_writer) 
  super(real_writer) 
  @line_number = 1 
 end 
  
 #The actual call is to WriterDecorator's write_line method, but with the number (decorated) in front of the written content 
 #That's why NumberingWriter decorates WriterDecorator's interface wirte_line 
 # 
 def write_line(line) 
  @real_writer.write_line("#{@line_number}:#{line}") 
  @line_number += 1 
 end 
end 
  
sw = SimpleWriter.new("numbering_write.txt") 
nw = NumberingWriter.new(sw) 
nw.write_line("hello,world") 
nw.write_line("hello,ruby") 
puts nw.line_number 
  
class CheckSummingWriter < WriterDecorator 
 attr_reader :check_num 
  
 def initialize(real_writer) 
  super(real_writer) 
  @check_num = 0 
 end 
  
 def write_line(line) 
  line.each_byte{|byte| @check_num += byte % 256} 
  @real_writer.write_line(line) 
 end 
end 
  
sw = SimpleWriter.new("check_num_writer.txt") 
csw = CheckSummingWriter.new(sw) 
csw.write_line("hello,world") 
puts csw.check_num 
  
class TimeStampingWriter < WriterDecorator 
  
 def initialize(real_writer) 
  super(real_writer) 
 end 
  
 def write_line(line) 
  @real_writer.write_line("#{Time.now}: #{line}") 
 end 
  
end 
  
#Reversed 
#5. The actual call is to SimpleWriter's write_line method, which writes the contents to the file 
sw = SimpleWriter.new("mix.txt") 
#4. The actual call is to the NumberingWriter's write_line method, which prefixes the input data with a number 
#4. The actual call is to NumberingWriter's write_line method, which prefixes the input data with a number, and then passes it to @real_writer, which is @real_witer as sw 
nw = NumberingWriter.new(sw) 
#3. the actual call is to the TimeStampingWriter write_line method, which prefixes the input data with a timestamp 
#4. then pass it to @real_writer, where @real_witer is nw 
tsw = TimeStampingWriter.new(nw) 
#2. The actual call is to CheckSummingWriter's write_line method, which counts the number of bytes of input data 
#2. The actual call is to the CheckSummingWriter write_line method, which counts the number of bytes of input data, and then passes it to @real_writer, which is @real_witer at this point 
csw = CheckSummingWriter.new(tsw) 
# 1. csw calls write_line 
csw.write_line("hello,world") 
puts csw.check_num 


ルビー風装飾パターンアプリケーション2種

(1)モジュールを混ぜるにはextendを使う

class SimpleWriter 
 def initialize(path) 
  @file = File.open(path,"w") 
 end 
 
 def write_line(line) 
  @file.print(line) 
  @file.print("\n") 
 end 
 
 #characters 
 def pos 
  @file.pos 
 end 
 
 # It will point the file pointer to the beginning of the file 
 def rewind 
  @file.rewind 
 end 
 
 def close 
  @file.colse 
 end 
end 
 
# Use the extend method to dynamically mix in modules for decoration 
 
module TimeStampingWriter 
 def write_line(line) 
  super("#{Time.now}:#{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 



最後に追加されるモジュールが最初に呼ばれ、その後、superを介して親クラスのwrite_lineメソッドが呼ばれます。 
この例では、まずテキストにタイムスタンプのプリフィックスを付け、数字を追加し、最後にファイルに書き込んでいます。  

sw = SimpleWriter.new("out3.txt") 
sw.extend(NumberingWriter) 
sw.extend(TimeStampingWriter) 
sw.write_line("hello,ruby") 



(2) aliasキーワードの使用

class SimpleWriter 
 def initialize(path) 
  @file = File.open(path,"w") 
 end 
 
 def write_line(line) 
  @file.print(line) 
  @file.print("\n") 
 end 
 
 #characters 
 def pos 
  @file.pos 
 end 
 
 # It will point the file pointer to the beginning of the file 
 def rewind 
  @file.rewind 
 end 
 
 def close 
  @file.colse 
 end 
end 
 
 
Another dynamic way to implement the decoration pattern in ruby. 
Modify the instance method of the object, so that the timestamp is added to the out1.txt file without affecting the object sw2, and the timestamp is not added to out2.txt. 
sw1 = SimpleWriter.new("out1.txt") 
class << sw1 
 alias old_write_line write_line 
 def write_line(line) 
  old_write_line("#{Time.now}:#{line}") 
 end 
end 
sw1.write_line("hello,world") 
sw2 = SimpleWriter.new("out2.txt") 
sw2.write_line("hello,world")