Rubyによるデザインパターン ~ その3

Rubyによるデザインパターンの内容をまとめようって試みですが、初っ端の10ページくらいをまとめて1つの記事にしちゃって、流石に楽をし過ぎた気がするのでもうちょっと書いてみます。

原則は前回まとめたので、今回は実際のパターンについて説明していきたいと思います。

ギャップを埋める: Adapter

ソフトウェアは思いつきで作られます。その為、世の中には互換性の無いオブジェクト(互いにたいわしたくてもインターフェースが合わないので出来ないオブジェクト)が腐るほどあります。そこで必要なのがAdapterです。

Adapterの例をコードで見てみましょう。例えば、ファイルを暗号化する既存のクラスがあるとします。

class Encrypter
  def initialize(key)
    @key = key
  end

  def encrypt(reader, writer)
    # readerとwriterはFileオブジェクト。readerの内容を暗号化してwriterに書き込む。読み込みはgetcメソッドで一文字ずつ行い、ファイルの終端判定はeof?メソッドで行う。
  end
end

Encryperクラスを使って一般的なファイルを簡単に暗号化する事が出来ます。2つのファイルを開き、選んだ秘密鍵を使ってEncrypterクラスのオブジェクトを生成し、encryptを呼ぶだけです。

reaer = File.open('message.txt')
writer = File.open('message.encrypted', 'w')
encrypter = Encrypter.new('my secret key')
encrypter.encrypt(reader, writer)
# この後、reader.closeとwriter.closeを行うべき

さて、それではファイルでは無く文字列を暗号化したい場合にはどうすれば良いでしょうか?こういった時にAdapterが活躍します。今回の例で言えば、外側からはFileオブジェクトに見えて(getcメソッドが使えて)、内部では文字列を扱うようなオブジェクトが必要です。そこで、StringOIAdapterを作ります。

class StringIOAdapter
  def initialize(strin)
    @string = string
    @position = 0
  end

  def getc
    return nil if @position >= @string.length
    ch = @string[@position]
    @position += 1
    return ch
  end

  def eof?
    @position >= @string.length
  end
end

reader = StringIOAdapter.new('message.txt')
writer = File.open('message.encrypted', 'w')
encrypter = Encrypter.new('my secret key')
encrypter.encrypt(reader, writer)

Fileオブジェクトが持つgetcメソッドやeof?メソッドを実装する事で、Fileオブジェクトと同じインターフェースを持つオブジェクトを作る事が出来ました。このように、Adapterは既存のインターフェースと必要なインターフェースとの間の深い溝を橋渡ししてくれます。

ちなみに、上記のコードはほぼオルセン本のままなのですが、encrypt内でgetcとeof?が使われるという実装の詳細に依存した構造になっている為、あまりよろしく無い気がします。で、ちょろっと調べたところRubyにはStringIOというクラスが標準ライブラリとして用意されてるみたいです。

require 'stringio'
sio = StringIO.new('hoge', 'r+')
sio.getc    # => "h"
sio.getc    # => "o"
sio.eof?    # => false
sio.getc    # => "g"
sio.getc    # => "e"
sio.eof?    # => true

理想通りの挙動ですね。このStringIOクラスも、Adapterであるといえるでしょう。

また、オルセン本では既に存在するクラスに拡張を行う事でAdapterの機能を果たす方法も紹介しています。例えば、

class BritishTextObject
  attr_reader :string, :size_mm, :colour
  
  # ...

end

に対して、外部からtextメソッド、size_inchesメソッド、colorメソッドを用いてアクセスしたいとしましょう。Adapterを使う場合には、

class BritishTextObjectAdapter < TextObject
  def initialzie(bto)
    @bto = bto
  end

  def text
    @bto.string
  end

  def size_inches
    @bto.size_mm / 25.4
  end

  def color
    @bto.clour
  end
end

のようなクラスを作る事になります。一方、Rubyの動的な性質を利用して、

require 'british_text_object'

class BritishTextObject
  def text
    string
  end

  def size_inches
    size_mm / 25.4
  end

  def color
    colour
  end
end

のように書く事も出来ます。ファイルの先頭のrequireで元のBritishTextObjectクラスをロードし、その後のclass BritishTextObject文でクラスを再オープンしていくつかのメソッドを追加で定義しています。わざわざAdapterクラスを作らなくてもインターフェースのギャップを埋める事が出来ました。


Rubyによるデザインパターン

Rubyによるデザインパターン