Rubyで学ぶデザインパターン

Factory メソッド

インスタンス生成をサブクラスに任せるデザインパターンインスタンスの生成部分を切り離すことで、結合度を下げて追加、変更、保守を容易にする。

例えば、次の楽器クラスがあるとする。

image.png

今後も楽器の種類が増えることが考えら、楽器インスタンスを生成するインタフェースが統一されていると、このクラスを利用するユーザは使いやすいし、保守もしやすくなる。

Factoryメソッドを次のように導入する。インスタンス化する際には、派生クラスで initialize メソッドを呼び出し、スーパクラスの initialize メソッドでは派生クラスの new メソッドを呼び出す。

そうすることで、Factoryクラスの各インスタンスに応じて生成するクラスを切り替えられる。

image.png

実行するときには、次のようにする

# サックスのファクトリーメソッド
factory = SaxophoneFactory.new(3)
saxophones = factory.ship_out

# トランペットのファクトリーメソッド
factory = TrumpetFactory.new(2)
trumpets = factory.ship_out

Abstract Factory メソッド

異なる組み合わせ を持った複数のオブジェクトをひと塊に生成するために用いる。

Abstract Factory メソッドの旨味は次の2点。

  • 関連し合うオブジェクトの集まりを生成できる
  • 整合性が必要なオブジェクト群を集めて生成できる

これだけだとよくわからんので、次のようなクラスがあるとする。

image.png

上記クラスを用いて開発することになったとする。その時の制約として、次のものがあるとする。

上記を守るように実装する際に、各クラスをそれぞれインスタンス化して、お互いが関係するように実装することもできるが、中々面倒。

そこで、AbstractFactory メソッドで、複数のオブジェクトをひと塊にして生成できるようにする。メソッドのイメージとしては次のような感じ。

image.png

インスタンス生成時には、スーパクラスの initialise メソッドを呼び出し、スーパクラスの initialize メソッドの中でインスタンス化を行う。そして、インスタンス化で呼び出される new メソッドでは、派生クラスの中で定義したものを用いる。

そうすることで、組み合わせを守ったオブジェクトを生成できる。

実行するときには、次のようなコードになる。

# カエルと藻のAbstractFactory
factory = FrogAndAlgaeFactory.new(4,1)
animals = factory.get_animals
animals.each { |animal| animal.eat }
plants = factory.get_plants
plants.each { |plant| plant.grow }

# アヒルとスイレンのAbstractFactory
factory = DuckAndWaterLilyFactory.new(3,2)
animals = factory.get_animals
animals.each { |animal| animal.eat }
plants = factory.get_plants
plants.each { |plant| plant.grow }

ビルダ(Builder) パターン

ビルダパターンは次の場面で使われる。

  • オブジェクト生成に大量のコードが必要(生成するインスタンスのプロパティが多い)
  • オブジェクトの生成が難しい
  • オブジェクト生成時に必要なチェックを行う

上記のために、ビルダパターンでは、『Director:作業過程を決定』と『Builder:作業インタフェースを持つ』を組み合わせて、柔軟にオブジェクトを生成するデザインパターンとなっている。

ビルダパターンには3つの構成要素がある。

役割名 内容
Director(ディレクタ) ビルダで提供されているインタフェースのみを使用して処理を行う
Builder(ビルダ) 各メソッドのインタフェースを定める
ConcreateBuilder(具体ビルダ) Builderが定めたインタフェースの実装

上だけだとよくわからないので、ここでもサンプルを考える。

加工水(塩水と砂糖水)を作る処理を考える。まずは実際の工程を考えてみると、必要な要素と作業に分割できそう。

項目 内容
要素 水、素材(砂糖 or 塩)
作業 水を加える、素材を加える

加工水を作るのに必要なモノと作業は揃った。あとはこれを実施する作業者がいれば加工水の生成が可能になる。つまり、オブジェクトが作れるようになったということ。

上記の例を、ビルダパターンに当てはめると、『要素:具体ビルダ』、『作業:ビルダ』、『作業者:ディレクタ』という関係になる。

要するに、オブジェクトの生成に必要な作業は、ビルダにインタフェースとして用意し、ディレクタで何をするかを決定していく。

そして、出来上がるオブジェクトは具体ビルダによって生成されるオブジェクトであり、それ自体の構成は、ディレクタが決定した作業(ビルダのインタフェースの組み合わせ)により決まる。

こうしておくことのメリットは、 ディレクタはインタフェース(作業)だけ知っていればよく、Builderではインタフェースを担保していれば良いので、すげ替えなどが容易 に行える。

クラス図に置き換えてみる。

image.png

ディレクタはビルダーのインタフェースを参照し、具体ビルダではビルダのインタフェースを実装する。

ソースコードで示すと次のような感じになる。

  • 具体ビルダ
# 塩水クラス (ConcreteBuilder:ビルダーの実装部分)
class SaltWater
  attr_accessor :water, :salt
  def initialize(water, salt)
    @water = water
    @salt = salt
  end

# 素材(塩)を加える
  def add_material(salt_amount)
    @salt += salt_amount
  end
end

# 砂糖水クラス (ConcreteBuilder:ビルダーの実装部分)
class SugarWater
  attr_accessor :water, :sugar
  def initialize(water, sugar)
    @water = water
    @sugar = sugar
  end

  # 素材(砂糖)を加える
  def add_material(sugar_amount)
    @sugar += sugar_amount
  end
end
  • ビルダ
# 加工した水を生成するためのインターフェイス(Builder)
class WaterWithMaterialBuilder
  def initialize(class_name)
    @water_with_material = class_name.new(0,0)
  end

  # 素材を入れる
  def add_material(material_amount)
    @water_with_material.add_material(material_amount)
  end

  # 水を加える
  def add_water(water_amount)
    @water_with_material.water += water_amount
  end

  # 加工水の状態を返す
  def result
    @water_with_material
  end
end
  • ディレクタ
# 加工水の作成過程を取り決める
class Director
  def initialize(builder)
    @builder = builder
  end

  def cook
    @builder.add_water(150)
    @builder.add_material(90)
    @builder.add_water(300)
    @builder.add_material(35)
  end
end

実際にオブジェクトを生成するときには、次のようにする。

# 砂糖水を作る
builder = WaterWithMaterialBuilder.new(SugarWater)
director = Director.new(builder)
director.cook

# 塩水を作る
builder = WaterWithMaterialBuilder.new(SaltWater)
director = Director.new(builder)
director.cook

シングルトンパターン

シングルトンパターンは、あるクラスのインスタンスが一つしかない状態を保証する方法。

例えば、次のようなリソース管理で用いるのが適切と考えられている。

  • ログの書き込み処理を行うメソッドでのファイルアクセス
  • システム内の共通のキャッシュテーブルを参照する場合
  • etc.

シングルトンパターンに則ったクラスの条件は次の3点。

Ruby でシングルトンパターンを実装する時には、 クラスに Singleton モジュールを Mix-in して利用する。

sigleton モジュールを Mix-in すると、new メソッドは プライベートになる。クラス図にすると次のような感じ。

image.png

上記のクラスを実装してみる。

# SingletonはMix-inしたクラスの同一のインスタンスを返す
require 'singleton'
# シングルトン
class SingletonObject
  # instanceメソッドが定義され、newメソッドがprivateに
  include Singleton
  attr_accessor :counter

  def initialize
    @counter = 0
  end
end

上のクラスを使ってみる。

obj1 = SingletonObject.instance
obj1.counter += 1
puts(obj1.counter) #=> 1

obj2 = SingletonObject.instance # 同一のインスタンスが返る
obj2.counter += 1
puts(obj2.counter)
# => 2 ⇦ 前回の結果が引き継がれている

また、シングルトンの条件として、『パブリックのコンストラクタを持たない』があるので、試しに new メソッドでインスタンス生成してみるとエラーになる。

obj3 = SingletonObject.new
# private method `new' called for SingletonObject:Class (NoMethodError)

アダプタ

アダプタは、現実世界の変換コネクタのようなもの。直接繋がらないコネクタと差し込み口は、変換コネクタを仲介として結びつける。つまり、クラスの型を整える『ラッパー』のようなもの。

アダプタが利用されるシーンは次のようなもの。

  • 関連性・互換性のないオブジェクト同士を結びつけたい
  • 他のコンポーネントへの変更ができるようにしたい

アダプタの構成要素は次の4種類。

項目 役割
利用者 ターゲットのメソッドを呼び出す
ターゲット インタフェースを規定
アダプタ アダプティのインタフェースを変換して、ターゲット向けのインタフェースを提供
アダプティ 実際に動作する既存クラス

ここも例の如く、クラス図っぽく書いてみる。

image.png

Client から OldPrinter のメソッドを用いるようにしたい時には、アダプタを用意してそこから呼び出したいメソッドを呼び出す。

まずは、ターゲットを実装。Ruby に Interface はないが、下記のようにオブジェクトを渡して、当該オブジェクトのメソッドを呼び出すという形で Interface を再現。

# 利用者(Client)へのインターフェイス (Target)
class Printer
  def initialize(obj)
    @obj = obj
  end

  def print_weak
    @obj.print_weak
  end

  def print_strong
    @obj.print_strong
  end
end

次に、アダプティを実装。

# Targetにはないインターフェイスを持つ (Adaptee)
class OldPrinter
  def initialize(string)
    @string = string.dup
  end

  # カッコに囲って文字列を表示する
  def show_with_paren
    puts "(#{@string})"
  end

  # アスタリスクで囲って文字列を表示する
  def show_with_aster
    puts "*#{@string}*"
  end
end

最後に、アダプタを実装。アダプタでは、インタフェースのメソッド内に呼び出したいメソッドを実装する。

# Targetが利用できるインターフェイスに変換 (Adapter)
class Adapter
  def initialize(string)
    @old_printer = OldPrinter.new(string)
  end

  def print_weak
    @old_printer.show_with_paren
  end

  def print_strong
    @old_printer.show_with_aster
  end
end

利用者では、Printer クラスのインスタンス生成時に、アダプタを用いてオブジェクト生成して渡す。

アダプタのメソッド内では、アダプティのメソッドが呼ばれるようになっているため、ターゲットのインタフェースで実行するとアダプティのメソッドが呼ばれる。

# 利用者(Client)
p = Printer.new(Adapter.new("Hello"))

# アダプタを通してアダプティのメソッドが呼ばれる
p.print_weaek # => (Hello)
p.print_strong # => *Hello*

コンポジットパターン

コンポジットパターンは、『全体と中身』を同一のものとして捉えることで、再起的な構造をクラスで表現するデザインパターンのこと。

コンポジットは3つの要素からなる。

要素名 役割
コンポーネント 全てのオブジェクトの基底となるクラス
リーフ プロセスの単純な構成要素で再帰しない
コンポジット コンポーネントの一つで、サブコンポーネントを構成

具体的な使い方としては、ディレクトリとフォルダを同様のコンポーネントとして扱うことで、削除処理などを再起的に行えるようにするなどがある。

他にも、ファイルシステムなどの木構造を伴う再起的なデータ構造を表現できたり、階層構造で表現されるオブジェクトの扱いが楽になる。

例として、ファイルシステムを考えてみる。まず、クラス図で全体像を眺める。ディレクトリは、再帰できる(自身の中にファイルとディレクトリをもてる)ので、集約でそれを表現する。

image.png

上記のコードを書く。

まずは、コンポーネントから。コンポーネントで共通メソッドを規定する。

# FileEntry, DirEntryクラスの共通メソッドを規定
class Entry
  def get_name
  end

  def ls_entry(prefix)
  end

  def remove
  end
end

次に、FileEntry クラス。

# Leaf
class FileEntry < Entry
  def initialize(name)
    @name = name
  end

  def get_name
    @name
  end

  def ls_entry(prefix)
    puts(prefix + "/" + get_name)
  end

  def remove
    puts @name + "を削除しました"
  end
end

最後に、DirEntry クラス。ディレクトリ内には、ファイルが複数あることもあるため、メンバ変数に配列がある。

# Composite
class DirEntry < Entry
  def initialize(name)
    @name = name
    @directory = Array.new
  end

  def get_name
    @name
  end

  def add(entry)
    @directory.push(entry)
  end

  def ls_entry(prefix)
    puts(prefix + "/" + get_name)
    @directory.each do |e|
      e.ls_entry(prefix + "/" + @name)
    end
  end

  def remove
    @directory.each do |i|
      i.remove
    end
    puts @name + "を削除しました"
  end
end

上記のコードを使ってみる。

root = DirEntry.new("root")
tmp = DirEntry.new("tmp")      # tmp ディレクトリ生成
tmp.add(FileEntry.new("conf")) # tmp ディレクトリ内にファイル追加①
tmp.add(FileEntry.new("data")) # tmp ディレクトリ内にファイル追加②
root.add(tmp)                  # root ディレクトリに tmp ディレクトリを追加

root.ls_entry("")
# => /root
# => /root/tmp
# => /root/tmp/conf
# => /root/tmp/data

root.remove
# => confを削除しました
# => dataを削除しました
# => tmpを削除しました
# => rootを削除しました

デコレータ

デコレータは、既存オブジェクトに簡単に機能を追加するためのパターン。デコレータパターンを適応することで、レイヤ状に機能を積み重ねて、必要な機能を持つオブジェクトを作れる。

レイヤ状に機能を重ねるので、既存オブジェクトの中身を変更なく機能を追加(デコレート)できる。機能の積み重ねの組み合わせの仕方で、様々な機能を実現できる。

継承に似ているが、継承よりも変更の影響を受けにくい。

デコレータは2つの要素からなる。

要素名 役割
具体コンポーネント ベースとなる処理を持つオブジェクト
デコレータ 追加する機能を持つクラス

『ファイルへの出力機能』を持つクラスで、デコレータの実装を学ぶ。まずは、クラス図を書いてみる。

image.png

上記の通り、基となる ConcreteComponent の機能を、デコレータで表現する。

SimpleWriter クラスを次ようなものとする。

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}")
        end
      end
    
  • タイムスタンプ出力機能を実装する

      class TimestampingWriter < WriterDecorator
        def write_line(line)
          @real_writer.write_line("#{Time.new} : #{line}")
        end
      end
    

上記を使って、コーディングしてみる。

f = NumberingWriter.new(SimpleWriter.new("file1.txt")) 
f.write_line("Hello out there")
f.close

f = TimestampingWriter.new(SimpleWriter.new("file2.txt"))
f.write_line("Hello out there")
f.close #=> 2012-12-09 12:55:38 +0900 : Hello out there

f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file3.txt")))
f.write_line("Hello out there")
f.close
# => 2012-12-09 12:55:38 +0900 : Hello out there

上記のように、既存クラス(ConcreteComponent)の実装を変更することなく、機能を自由に組み合わせることが可能。

プロキシ

プロキシパターンは、1つオブジェクトに複数の関心事がある場合に、それを分離したい時に使う。

例えば、オブジェクトの本質的な目的とは異なる非機能用件(セキュリティ要件とか)を切り離して実装できる。

プロキシは2つの要素からなる。

要素名 役割
対象オブジェクト(subject) 本物のオブジェクト
代理サブジェクト(proxy) 特定の「関心事」を担当、それ以外を対象サブジェクトに渡す

イメージ的には、代理人が本人でなくてもできることを処理し、代理人ができない本人が処理するといった、いわゆる『代理人』のように振る舞う。

代理オブジェクトは、対象オブジェクトと同じインターフェースを持ち、利用時は代理オブジェクトを通して対象となるオブジェクトを操作する

サンプルとして、銀行の窓口業務を担当するクラスとユーザ認証を担当するクラスで、『関心事の分離』をするプロキシパターンを学ぶ。

イメージ的には、ユーザ認証担当は、代理人として自身にできること(ユーザ認証)は自分で処理して、できないこと(入金/出金)は対象の窓口担当に回すって感じ。

まずは、クラス図を書いてみる。

image.png

上記のように、クライアントは代理オブジェクトにリクエストを出すことで、銀行内のオブジェクトを操作できる。代理オブジェクトはできることは自分で行い、できないことは対象オブジェクトのメソッドを呼び出し対象オブジェクトに処理させる。

コードは次のような感じ。

# 銀行の入出金業務を行う(対象オブジェクト/subject)
class BankAccount
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  # 出金
  def deposit(amount)
    @balance += amount
  end

  # 入金
  def withdraw(amount)
    @balance -= amount
  end
end

次に代理人オブジェクト。

ポイントは、 initialize で対象オブジェクトを渡している点。

# etcはRubyの標準ライブラリで、/etc に存在するデータベースから情報を得る
# この場合は、ログインユーザー名を取得するために使う
require "etc"

# ユーザーログインを担当する防御Proxy
class BankAccountProxy
  def initialize(real_object, owner_name)
    @real_object = real_object
    @owner_name = owner_name
  end

  def balance
    check_access
    @real_object.balance
  end

  def deposit(amount)
    check_access
    @real_object.deposit(amount)
  end

  def withdraw(amount)
    check_access
    @real_object.withdraw(amount)
  end

  def check_access
    if(Etc.getlogin != @owner_name)
      raise "Illegal access: #{@owner_name} cannot access account."
    end
  end
end

BankAccountProxy にとって、 check_access 以外は自身の主目的でないので、対象オブジェクトのメソッドを呼び出す設計になっている。

で、使う時には、次のように書く。

# ログインユーザーの場合
account = BankAccount.new(100)
proxy = BankAccountProxy.new(account, "goruchan")
puts proxy.deposit(50)  # => 150    <- proxyを通してオブジェウトを操作
puts proxy.withdraw(10) # => 140

# ログインユーザーではない場合
account = BankAccount.new(100)
proxy = BankAccountProxy.new(account, "no_login_user")
puts proxy.deposit(50)
#`check_access': Illegal access: no_login_user cannot access account. (RuntimeError)

proxynew するときに、対象オブジェクトと所有者を引数として渡し、組としている。

コマンド

コマンドは、あるオブジェクトに対してコマンドを送ることで、そのオブジェクトのメソッドを呼び出すこと。

例えば、Linuxコマンドのように、ユーザはファイルシステムの実装を知らなくても、ファイルの追加・削除を実行できるのも、コマンドパターンの一つ。

コマンドパターンは、2つの要素からなる。

要素名 役割
コマンド コマンドのインタフェース
具体コマンド コマンドの具体的な処理

ファイルの作成・削除・コピーができるモデルで、コマンドパターンを考える。

まずはクラス図で書いてみる。

image.png

基本的には、継承先で基底クラスのメソッドをオーバライドする。

コマンドのコードは次のような感じ。

class Command
  attr_reader :description
  def initialize(description)
    @description = description
  end

  def execute
  end

  def undo_execute
  end
end

具体コマンドを実装していく。

require "fileutils"

class CreateFile < Command
  def initialize(path, contents)
    super("Create file : #{path}")
    @path = path
    @contents = contents
  end

  def execute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end

  def undo_execute
    File.delete(@path)
  end
end

class DeleteFile < Command
  def initialize(path)
    super("Delete file : #{path}")
    @path = path
  end

  def execute
    if File.exists?(@path)
      @content = File.read(@path)
    end
    File.delete(@path)
  end

  def undo_execute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end
end

class CopyFile < Command
  def initialize(source, target)
    super("Copy file : #{source} to #{target}")
    @source = source
    @target = target
  end

  def execute
    FileUtils.copy(@source, @target)
  end

  def undo_execute
    File.delete(@target)
    if(@contents)
      f = File.open(@target, "w")
      f.write(@contents)
      f.close
    end
  end
end

class CompositeCommand < Command
  def initialize
    @commands = []
  end

  def add_command(cmd)
    @commands << cmd
  end

  def execute
    @commands.each { |cmd| cmd.execute }
  end

  def undo_execute
    @commands.reverse.each { |cmd| cmd.undo_execute }
  end

  def description
    description = ""
    @commands.each { |cmd| description += cmd.description + "\n"}
    description
  end
end

クラスの準備はこれで完了。あとはこいつを使ってみる。

command_list = CompositeCommand.new
command_list.add_command(CreateFile.new("file1.txt", "hello world\n"))
command_list.add_command(CopyFile.new("file1.txt", "file2.txt"))
command_list.add_command(DeleteFile.new("file1.txt"))

command_list.execute
puts(command_list.description)
# => Create file : file1.txt
# => Copy file : file1.txt to file2.txt
# => Delete file : file1.txt

# 処理を取り消すコマンド
command_list.undo_execute
# => file1のみになっている

インタプリタ

インタプリタは、ひとつひとつの問題はシンプルだが、組み合わさって複雑になるような場合に効果を発揮する。

インタプリタでは、各問題を処理し、その結果を手順に基づいて処理を実行していく。

上だけだと何言ってるかわからないけれど、例えば次のような機能を実装したいとする。

  • ファイルネームが〇〇、かつ、サイズが△△以上のファイル一覧を表示せよ
  • ファイルネームが〇〇、または、サイズが△△以上のファイル一覧を表示せよ
  • ファイルネームが〇〇以外、かつ、サイズが△△以上のファイル一覧を表示せよ

上記の場合、具体的な機能(ファイルネーム検索、サイズ確認)と、それを組み合わせる処理(And,Or,Not)があることで、少し複雑になっていて、まさにインタプリタの効果を発揮するパターンといえる。

インタプリタパターンは、4つの要素からなる。

要素名 役割
抽象表現(AbstractExpression) 共通のインタフェースを定義
終端(TerminalExpression) 終端を表現するクラス(具体的な機能)
終端以外(NonterminalExpression) 非終端を表現するクラス(組み合わせ)
状況、文脈(Context) 構文の解析を手助けする

ファイル検索機能でインタプリタを学んでいく。

とりあえず、クラス図で書いてみる。

image.png

まずは、すべてのファイル検索のベースとなる単純なクラスを用意する。

class Expression
  def |(other)
    Or.new(self, other)
  end

  def &(other)
    And.new(self, other)
  end
end

次に、具体的な機能のクラスを用意

  • すべてのファイル名を取得する All クラス
require "find"

class All < Expression
  def evaluate(dir)
    results= []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p
    end
    results
  end
end
  • 与えられたパターンとマッチするすべてのファイル名を返す FileName クラス
class FileName < Expression
  def initialize(pattern)
    @pattern = pattern
  end

  def evaluate(dir)
    results= []
    Find.find(dir) do |p|
      next unless File.file?(p)
      name = File.basename(p)
      results << p if File.fnmatch(@pattern, name)
    end
    results
  end
  • 指定したファイルサイズより大きいファイルを返す Bigger クラス
class Bigger < Expression
  def initialize(size)
    @size = size
  end

  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p if( File.size(p) > @size)
    end
    results
  end
end
  • 書込可能なファイルを返す Writable クラス
class Writable < Expression
  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p if( File.writable?(p) )
    end
    results
  end
end

そして、組み合わせ部分のクラスを用意

  • 書き込みができないファイル検索用の Not クラス
class Not < Expression
  def initialize(expression)
    @expression = expression
  end

  def evaluate(dir)
    All.new.evaluate(dir) - @expression.evaluate(dir)
  end
end
  • ファイル検索式結合用の Or, And クラス
class Or < Expression
  def initialize(expression1, expression2)
    @expression1 = expression1
    @expression2 = expression2
  end

  def evaluate(dir)
    result1 = @expression1.evaluate(dir)
    result2 = @expression2.evaluate(dir)
    (result1 + result2).sort.uniq
  end
end

class And < Expression
  def initialize(expression1, expression2)
    @expression1 = expression1
    @expression2 = expression2
  end

  def evaluate(dir)
    result1 = @expression1.evaluate(dir)
    result2 = @expression2.evaluate(dir)
    (result1 & result2)
  end
end

上のクラスを用いて、ファイル検索をすると、次のような出力になる。

# 具体的な機能(名前検索)を組み合わせ(And)で結合
complex_expression1 = And.new(FileName.new('*.mp3'), FileName.new('big*'))
puts complex_expression1.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3

# 具体的な機能(サイズ検索)だけ
complex_expression2 = Bigger.new(1024)
puts complex_expression2.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
#=> 13_test_data/subdir/other.mp3

# 具体的な機能(名前検索)で条件に当てはまるオブジェクト生成し、Andで抽出。
complex_expression3 = FileName.new('*.mp3') & FileName.new('big*')
puts complex_expression3.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3

# 具体的な機能(全抽出)だけ
complex_expression4 = All.new
puts complex_expression4.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
#=> 13_test_data/small.mp3
#=> 13_test_data/small1.txt
#=> 13_test_data/small2.txt
#=> 13_test_data/subdir/other.mp3
#=> 13_test_data/subdir/small.jpg

イテレータ

イテレータは次のような場合に用いる。

  • 要素の集まったオブジェクト(配列等)にアクセス
  • 集合の要素に順番にアクセスする必要がある

つまり、イテレータパターンでは、要素の集まりを保有するオブジェクトの各要素に、順番にアクセスする方法を提供するパターンといえる。

イテレータパターンには、内部イテレータと外部イテレータがある。

イテレータ 概要
内部イテレータ コードブロックベースのイテレータeach メソッドがこれに相当する
外部イテレータ コードブロック外のクラスとして用意したイテレータ

ブログ内の記事にアクセスする方法で、外部イテレータについて学んでいく。

とりあえず、クラス図で書いてみる。

image.png

外部イテレータは、ブログ(集約オブジェクト)の要素(記事)にアクセスするためのパターン。

まずは、要素の記事のクラスを書く。

class Article
  def initialize(title)
    @title = title
  end

  attr_reader :title
end

次に、要素のまとまりであるブログクラスを書く。

class Blog
  def initialize
    @articles = []
  end

  def get_article_at(index)
    @articles[index]
  end

  def add_article(article)
    @articles << article
  end

  def length
    @articles.length
  end

  def iterator
    BlogIterator.new(self)
  end
end

最後に、このまとまりを操作するイテレータを用意する。

class BlogIterator
  def initialize(blog)
    @blog = blog
    @index = 0
  end

  def has_next?
    @index < @blog.length
  end

  def next_article
    article = self.has_next? ? @blog.get_article_at(@index) : nil
    @index = @index + 1
    article
  end
end

上記の動作を確認する。

blog = Blog.new
blog.add_article(Article.new("デザインパターン1"))
blog.add_article(Article.new("デザインパターン2"))
blog.add_article(Article.new("デザインパターン3"))

iterator = blog.iterator
while iterator.has_next?
  article = iterator.next_article
  puts article.title
end
#=> デザインパターン1
#=> デザインパターン2
#=> デザインパターン3

オブザーバ

オブザーバは、次の条件を満たす場合に用いる。

  • オブジェクトの状態が変化する可能性がある
  • 変化したことを他のオブジェクトに通知する必要がある

例えば、新幹線の予約者が予約を解除したいときに、予約キャンセルを鉄道会社に連絡するみたいな状況。

上の例の通り、オブザーバは、あるオブジェクトの状態が変化した際に、オブジェクト自身が『観察者』に『通知』する仕組み。これにより、観察者は、常に観察しなければならない状態から解放される。

オブザーバパターンは、3つのオブジェクトからなる。

オブジェクト名 役割
サブジェクト(subject) 変化する側のオブジェクト(変化を報告する側)
オブザーバ(Observer) 状態の変化を関連するオブジェクトに通知するインタフェース
具象オブザーバ(ConcreteObserver) 状態の変化に関連して具体的な処理を行う(報告を受ける側)

従業員の小切手、税金請求書発行の処理で、オブザーバについて学んでいく。

とりあえず、クラス図で書いてみる。

image.png

まずは、サブジェクトのコードを書く。

require 'observer'
class Employee
  include Observable
  attr_reader :name, :title, :salary

  def initialize(name, title, salary)
    @name = name
    @title = title
    @salary = salary
    add_observer(Payroll.new)
    add_observer(TaxMan.new)
  end

  def salary=(new_salary)
    @salary = new_salary
    changed
    notify_observers(self)
  end
end

ruby には、observable と呼ばれるオブザーブとしての機能を持つ標準モジュールがあるため、組み込む。

changed メソッドで更新フラグを立て、add_observer で追加したオブザーブに対して、notify_observers メソッドで更新通知を出す。

次に、小切手発行と、税金請求書発行を行うクラスのコードを書く。

class Payroll
  def update(changed_employee)
    puts "彼の給料は#{changed_employee.salary}になりました!#{changed_employee.title}のために新しい小切手を切ります。"
  end
end

class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります"
  end
end

これを実行する。

john = Employee.new('John', 'Senior Vice President', 5000)
john.salary = 6000
#=> 彼の給料は6000になりました!Senior Vice Presidentのために新しい小切手を切ります。
#=> Johnに新しい税金の請求書を送ります
john.salary = 7000
#=> 彼の給料は7000になりました!Senior Vice Presidentのために新しい小切手を切ります。
#=> Johnに新しい税金の請求書を送ります

salary を更新したタイミングで、具象オブザーバが呼ばれていることが確認できる。

ストラテジパターン

ストラテジパターンは、メソッドの中である一部分の処理を切り替えたい時に使えるパターン。状況に応じて、アルゴリズムを変えなければならないような時に使える。例えば、ゲームの難易度設定に応じて、戦略アルゴリズムが変わるなど。

ストラテジパターンは、3つのオブジェクトからなる。

オブジェクト名 役割
コンテキスト(Context) ストラテジの利用者
抽象戦略(Strategy) 同じ目的をもった一連のオブジェクトを抽象化したもの
具象戦略(ConcreteStrategy) 具体的なアルゴリズム

ストラテジのアイディアは、コンテキスト*1が委譲*2によってアルゴリズムを交換できるようにすること。

レポート生成を、HTML形式とプレーンテキスト形式で切り替えられるようなプログラムを題材に、ストラテジについて学んでいく。

とりあえず、クラス図で書いてみる。

image.png

まずは、両形式のインタフェースとなる、抽象戦略のコードを書く。

class Formatter
  def output_report(title, text)
    raise 'Called abstract method !!'
  end
end

次に、具象戦略のコードを書く。

class HTMLFormatter < Formatter
  def output_report(report)
    puts ''
    report.text.each { |line| puts "#{line}" }
    puts ''
  end
end

class PlaneTextFormatter < Formatter
  def output_report(report)
    puts "***** #{report.title} *****"
    report.text.each { |line| puts(line) }
  end
end

最後に、レポートのコードを書く。

class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(formatter)
    @title = 'report title'
    @text = %w(text1 text2 text3)
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self)
  end
end

initialize メソッドで @formatterインスタンス生成時に渡した具象戦略を設定して、インスタンスのレポート出力は、具象戦略の output_report に自分自身(Report)を渡して処理させている。

結果を確認する。

report = Report.new(HTMLFormatter.new)
report.output_report
# =>
# => text1
# => text2
# => text3
# => 

# formatterの出力形式を切り替える(戦略変更)
report.formatter = PlaneTextFormatter.new
report.output_report
# => ***** report title *****
# => text1
# => text2
# => text3

テンプレートメソッド

テンプレートメソッドは、2つのコードのやりたいこと(アルゴリズム)がほぼ同じで、部分的に変更したい時に有効なパターン。

スーパークラスで処理の枠組みを固め、サブクラスで具体的内容を実装する。

スーパークラスでは、アルゴリズムの流れの中で利用される抽象的なメソッドと、抽象的なメソッドを用いて処理のアルゴリズムを定義するテンプレートメソッドを定義する。

テンプレートメソッドを使うことのメリットは次のもの。

テンプレートメソッドは、2つのオブジェクトからなる。

オブジェクト名 役割
スーパークラス 変わらない基本的なアルゴリズム
サブクラス 具体的な処理

レポート生成を、HTML形式とプレーンテキスト形式で切り替えられるようなプログラムを題材に、テンプレートメソッドについて学んでいく。

とりあえず、クラス図で書いてみる。

image.png

まずは、スーパークラスで高レベルの処理を書く。

# レポートを出力する
class Report
  def initialize
    @title = "html report title"
    @text = ["report line 1", "report line 2", "report line 3"]
  end

  # レポートの出力手順を規定
  def output_report
    output_start
    output_body
    output_end
  end

  # レポートの先頭に出力
  def output_start
  end

  # レポートの本文の管理
  def output_body
    @text.each do |line|
      output_line(line)
    end
  end

  # 本文内のLINE出力部分
  def output_line(line)
    raise 'Called abstract method !!'
  end

  # レポートの末尾に出力
  def output_end
  end
end

次に、サブクラスで詳細な処理を記述する。

# HTML形式でのレポート出力を行う
class HTMLReport < Report
  def output_start
    puts ''
  end

  def output_line(line)
    puts "#{line}"
  end

  def output_end
    puts ''
  end
end

# PlaneText形式(`*****`で囲う)でレポートを出力
class PlaneTextReport < Report
  def output_start
    puts "**** #{@title} ****"
  end

  def output_line(line)
    puts line
  end
end

使用する際には、サブクラスをインスタンス化するだけ。

html_report = HTMLReport.new
html_report.output_report
# => 
# => report line 1
# => report line 2
# => report line 3

plane_text_report = PlaneTextReport.new
plane_text_report.output_report
# => **** html report title ****
# => report line 1
# => report line 2
# => report line 3

参考

*1:同じコード記述やプログラム上の要素が、その置かれているプログラム内での位置や、実行される際の内部状態などによって異なる振る舞いをしたり、異なる制約を受けたりすること

*2:ある機能を持つオブジェクトを生成して、オブジェクトに処理を依頼する

*3:抽象度の高い処理、ロジック的な部分、処理のフレーム

*4:レポート行を書き出すと言うような具体的な処理