784
793

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rubyによるデザインパターンまとめ

Last updated at Posted at 2018-09-10

はじめに

デザインパターンって常識っぽいからちゃんと学んでおかないとと思いつつ、いまいちよく分からないままな人って意外と多いんじゃないかと思い、絶版になってることもありまとめてみることに。2章は基本的なRubyの文法の解説なので省略。
変なところあったら教えてくださいね。

対象

  • デザインパターンを避けてきた人
  • Ruby以外が不得意で、巷に多く出回っているJavaなどのデザインパターンの本が読みたくても読めない、もしくは読むのが面倒な人
  • Rubyによるデザインパターン買いそびれた人

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

  • ラス・オルセン 著、ピアソン・エデュケーション(2009)
  • GoFによる23のデザインパターンのうち著者が特に有益だと考える14パターン + Ruby独自の3パターンの解説が書かれている。
  • 実際にデザインパターンが使われている例として、RubyやRailsの内部のコードが出てくる。楽しい。
  • Rubyの力を活かして書かれている場合がある。大抵は他の言語で実装する場合より少し楽に書ける。基底クラス書かなかったり、メソッドを自動で委譲したり。
  • 中古しか出回ってなくて高い。Amazonで見たら1万円超えてた!

1章 よいプログラムとパターン

デザインパターンそのものについての解説や、デザインの観点からみた良質なプログラミングとは、デザインパターンを使うことでなにが達成できるか、などが書かれている。

GoF

デザインパターンの紹介的な意味合いで、GoFの『オブジェクト指向における再使用のためのデザインパターン』を紹介している。
GoF本の冒頭で書かれているプログラミングの一般的な原則についての、筆者なりの要約が以下。

  • 変わるものを変わらないものから分離する
  • インターフェイスに対してプログラムし、実装に対して行わない
  • 継承より集約
  • 委譲,委譲,委譲

さらに筆者が考える重要なポイントとして

  • 必要になるまで作るな

を挙げ、続く章内で上記5点のポイントについて解説している。

変わるものを変わらないものから分離する

理想的なシステムでは、全ての変更は局所的。変わりやすいと思われるものを変わりにくいと思われるものから分離すべき。

インターフェイスに対してプログラムし、実装に対して行わない

# 自動車と飛行機を扱う
if is_car
  my_car = Car.new
  my_car.drive(200)
else
  my_plane = Plane.new
  my_plane.fly(200)
end

このコードは自動車と飛行機の両方と結合してしまっており、もっと乗り物が増えると対応し切れない。
1つの共通のインターフェイスを実装すれば、次のように改善できる。

my_vehicle = get_vehicle
my_vehicle.travel(200)

このコードはオブジェクト指向プログラミング的に望ましいだけでなく、インターフェイスに対するプログラミングという原則の例にもなっている。元のコードは自動車と飛行機しか扱えなかったが、改善されたコードではあらゆる乗り物を使える。

※ここでいうインターフェイスとは、Javaなどで使用されるあの"interface"ではない。
インターフェイスに対してプログラムせよとは、可能な限り一般的な型に対してプログラミングせよということ。

継承より集約

継承を使えばコストをかけずに実装を手に入れられるが、同時に望ましくない繋がりも作ってしまう。
それぞれが強く結合されていないシステムを構築したいのなら、継承に頼りすぎてはいけない。
その代わりに、集約を使って実装を手に入れる方法がある。
集約とは...

オブジェクトに、他のオブジェクトの参照を持たせること。
実装は参照先のオブジェクトにカプセル化されているため、必要な機能があればそこから呼び出すことが出来る。
要するに、オブジェクトが何かの1種である(is-a-kind-of)関係は避けて、何かを持っている(has-a)関係にするということ。

継承と集約をコードで比べてみる。

class Vehicle
  # 乗り物に関するたくさんのコード...

  def start_engine
    # エンジンをスタート
  end

  def stop_engine
    # エンジンをストップ
  end
end

class Car < Vehicle # 継承
  def sunday_drive
    start_engine
    # 地方に出かけて、戻ってくる
    stop_engine
  end
end

エンジンに関するコードを共通の基底クラスであるVehicleとして抽象化し、色んな乗り物クラスで継承出来るようにしている。
しかし、この設計だとまず全ての乗り物がエンジンを持っていることが必要になってしまう。
また、エンジンの詳細はCarクラスに筒抜けになってしまっている。変わりやすい部分を変わりにくい部分から分離できていない。
これらの問題を集約で回避するために、エンジンに関するコードをCarのスーパークラスの中に作るのではなく、完全に独立したクラスとして作ってみる。

class Engine
  # エンジンに関するたくさんのコード...

  def start
    # エンジンをスタート
  end

  def stop
    # エンジンをストップ
  end
end

class Car
  def initialize
    @engine = Engine.new # 集約
  end

  def sunday_drive
    @engine.start
    # 地方に出かけて、戻ってくる
    @engine.stop
  end
end

集約を使って機能を組み立てることで、多くの利点を得ることが出来る。
まず、エンジンに関するコードが、エンジンという本来のクラスの中に移動したので、再利用性が高まる。
さらに、Vehicleからエンジンに関するコードを取り出すことによって、Vehicleクラスをシンプルに出来る。
また、カプセル化も促進している。Vehicleからエンジンに関するコードを分離したことで、自動車とエンジンの間にインターフェイスの厚い壁ができている。
さらにさらに、他の種類のエンジンも利用できるようになっている。Engineクラスを抽象的な型にし様々なエンジンに継承させれば、次のように自由にエンジンを利用するようにもできるし、途中でエンジンの種類を切り替えることだって出来る。

class Car
  def initialize(engine)
    @engine = engine
  end

  # 中略...

  def switch_to_diesel
    @engine = DieselEngine.new
  end
  
  def switch_to_gasoline
    @engine = GasolineEngine.new
  end
end

委譲,委譲,委譲

class Car
  def initialize
    @engine = GasolineEngine.new
  end

  def start_engine
    @engine.start
  end

  def stop_engine
    @engine.stop
  end
end

上記のように集約と組み合わせて他のクラスに「仕事を押し付ける」ことが出来る。
継承に対するとても強力で柔軟な代替手段になりうる。
上記のようにstart_engineやstop_engineなどのメソッドをいちいち書くのは面倒だが、Rubyでは便利な方法が用意されている。(method_missingをオーバーライドするやり方 : 10章参照、forwardableモジュールを使うやり方 : 11章参照)

必要になるまで作るな

いわゆるYAGNI(You Ain't Gonna Need It: 必要になるまで作るな)。
デザインパターンを学習するとオーバーエンジニアリングをしがち。
パターンは便利なテクニックだが、それを実装すること自体が目的にならないように常に注意する必要がある。

3章 アルゴリズムを変更する : Template Method

あなたのコードは44行目を除いてまったく同じことをさせたいのかもしれません。
44行目はこっちをしたいときもあるし、あっちをしたいこともあります。
そのような時はTemplate Methodが使えます。

Template Methodとは、継承を使ったパターンである。
変わらない部分を親クラスに記述する。そして変わる部分を子クラスに書くことでカプセル化する。

例として、レポートジェネレーターを作るケースを考えてみる。レポートはHTML形式でフォーマットする必要があるとする。
まずは駄目な例をつらつら。

class Report
  def initialize
    @title = '月次報告'
    @text = ['順調', '最高の調子']
  end

  def output_report
    puts "<title>#{@title}</title>"
    puts '<body>'
    @text.each do |line|
      puts "<p>#{line}</p>"
    end
    puts '</body>'
  end
end

目的が基本的なHTMLを生成するだけで、今後変更がないならこれで問題ない。

report = Report.new
report.output_report

とやれば動いてくれる。
しかしここに変更が訪れて、プレーンテキストのレポートを出力する必要が出てきたとする。
そんな時に、

class Report
  # 省略

  def output_report
    if format == :plane
      puts("***#{@title}***")
      @text.each do |line|
        puts("***#{line}***")
      end
    else
      puts "<title>#{@title}</title>"
      puts '<body>'
      @text.each do |line|
        puts "<p>#{line}</p>"
      end
      puts '</body>'
    end
  end
end

上記のようにやるのはよくない。変化する部分と変化しない部分が混ざりあっている。
ごちゃごちゃしているし、もっとフォーマットが増えると壊れる。
こんな時、Template Methodを使った上手いやり方がある。
まずは一旦時間を巻き戻してHTMLについてだけを考える。さっきと違い処理の流れを整理してメソッドに切り分けてみる。

class Report
  def output_report
    output_title
    output_body_start
    output_body
    output_body_end
  end

  def output_title
    puts "<title>#{@title}</title>"
  end

  def output_body_start
    puts '<body>'
  end

  def output_body_end
    puts '</body>'
  end

  def output_body
    @text.each do |line|
      puts "<p>#{line}</p>"
    end
  end
end

そして変更が訪れて、プレーンテキストのレポートを出力することに。
ここで初めてTemplate Methodを使う。(HTMLの出力しかしないでいい時点ではまだサブクラスに切り分けなくてよい。パターンを学ぶとオーバエンジニアリングをしがちなので気をつける。)

class Report
  def output_report
    output_title
    output_body_start
    output_body
    output_body_end
  end

  # Rubyにはabstractメソッド(抽象メソッド)がないので、以下のようにしてサブクラスでの実装を強制する
  def output_title
    raise 'Called abstract method: output_title'
  end

  def output_body
    raise 'Called abstract method: output_body'
  end

  # 以下の2つのメソッドをフックメソッドという。すぐ下で解説。
  def output_body_start
  end

  def output_body_end
  end
end

class HTMLReport < Report
  def output_title
    puts "<title>#{@title}</title>"
  end

  def output_text
    @text.each do |line|
      puts "<p>#{line}</p>"
    end
  end

  def output_body_start
    puts '<body>'
  end

  def output_body_end
    puts '</body>'
  end
end

class PlainReport < Report
  def output_title
    puts("***#{@title}***")
  end

  def output_text
    @text.each do |line|
      puts("***#{line}***")
    end
  end
end

これで完成。変わらない部分はレポートクラスに記述し、変わる部分をサブクラスに書いている。

report = HTMLReport.new
report.output_report

report2 = PlainReport.new
report2.output_report

などと出来るようになった。サブクラスを増やすことで他のフォーマットにも対応出来る。

フックメソッド

Template Methodの具象クラス(今回でいうHTMLReportとかPlainReport)によってオーバーライド出来る非抽象メソッドのこと。
Template Methodの習得には欠かせないので、一緒に覚えるとよい。
フックメソッドを基底クラス(今回でいうReport)で定義することによって、サブクラスは基底クラスの実装をそのまま使うか、オーバーライドして独自の実装をするか選べる。
今回の場合はoutput_body_startとoutput_body_endはPlainReportクラスでは使われないので、空のフックメソッドを基底クラスに定義して置くことで、スマートな実装が可能になる。
ちなみに今回は空のフックメソッドだったが、共通するような処理であれば、基底クラスであらかじめ実装しておくとよい。

要点

  • Template Methodパターンは、アルゴリズムに多様性をもたせたい場合にとても便利。
  • 基底クラスに不変の部分を記述し、変わる部分はサブクラスに定義するメソッドにカプセル化する。
  • 基底クラスはメソッドを未定義にしておくことができる。その場合はサブクラスでそのメソッドを提供しなければならない。
  • 未定義にする代わりに、基底クラスで標準実装を提供し(フックメソッド)、サブクラスはそのまま使うかオーバライドするか選ぶことも出来る。
  • 他のパターンでも表れる基本的なオブジェクト指向技術である。例えば13章のFactory Methodパターンは、新しいオブジェクトの生成に対してTemplate Methodパターンを適用したもの。

4章 アルゴリズムを交換する : Strategy

ひょっとすると、変わるのは44行目ではなくてアルゴリズム全体なのかもしれません。
ちゃんと仕事は定義されていて、完了させる必要があるのですが、方法がたくさんあるのです。
ネコを檻から出す仕事があった時、それには1つ以上のテクニックがあるかもしれません。
それらのテクニックやアルゴリズムをラップするものが、Strategyオブジェクトです。

前章のTemplate Methodパターンにはいくつか欠点がある。
継承を使っているので、サブクラスが基底クラスに依存してしまっている。また、出力形式を切り替えたくなった際に方針を変えることが難しい。
今度は継承の代わりに委譲を使い、バリエーションごとにサブクラスを作る代わりに、変化するコードをクラスに閉じ込める方法をとってみる。一旦駄目な例を再び。

class Report
  def initialize
    @title = '月次報告'
    @text = ['順調', '最高の調子']
  end

  def output_report
    if format == :plane
      puts("***#{@title}***")
      @text.each do |line|
        puts("***#{line}***")
      end
    else
      puts "<title>#{@title}</title>"
      puts '<body>'
      @text.each do |line|
        puts "<p>#{line}</p>"
      end
      puts '</body>'
    end
  end
end

このフォーマットの部分をクラスに切り分ける。

class Report
  attr_reader :title, :text
  attr_accessor :formatter # 集約

  def initialize(formatter)
    @title = '月次報告'
    @text = ['順調', '最高の調子']
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self) # 委譲
  end
end

class Formatter
  def output_report(context)
    raise 'Abstract method called'
  end
end

class HTMLFormatter < Formatter
  def output_report(context)
    puts "<title>#{context.title}</title>"
    puts '<body>'
    context.text.each do |line|
      puts "<p>#{line}</p>"
    end
    puts '</body>'
  end
end

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

これでGoF流のStrategyパターンは早くも完成(Ruby流のやり方をあとで2つ紹介している)。
Strategyパターンの核となるアイデアは、同じ目的を持った一群のオブジェクト、つまりストラテジを定義すること。今回の場合は、レポートのformatがストラテジ。
それぞれのストラテジ(今回の場合HTMLFormatterとPlainFormatter)は同じ仕事をこなすだけでなく、その全てが正確に同じインターフェイスを提供する。今回の場合output_reportメソッドをどちらも持っている。
これにより、クラスの外部からはストラテジオブジェクトが同じに見え、ストラテジの利用者(GoFではcontextと呼ぶ)はそれらを取り替え可能なパーツとして扱うことが出来るようになる。

report = Report.new(HTMLFormatter.new)
report.output_report

report.formatter = PlainTextFormatter.new
report.output_report

こんな風に切り替えも楽に出来る。
さらにRuby流として、単純にFormatterを消すリファクタが出来る。
HTMLFormatterもPlainTextFormatterもoutput_reportを実装している。
ここで本質的には何もしない基底クラスを作るのは、Ruby流のダックタイピングの哲学に反する。なので、

class Report
  attr_reader :title, :text
  attr_accessor :formatter # 集約

  def initialize(formatter)
    @title = '月次報告'
    @text = ['順調', '最高の調子']
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self) # 委譲
  end
end

class HTMLFormatter
  def output_report(context)
    puts "<title>#{context.title}</title>"
    puts '<body>'
    context.text.each do |line|
      puts "<p>#{line}</p>"
    end
    puts '</body>'
  end
end

class PlainTextFormatter
  def output_report(context)
    puts("***#{context.title}***")
    context.text.each do |line|
      puts("***#{line}***")
    end
  end
end

こんな感じでスッキリ。使い方は全く変わらない。
さらに、コードのかたまりを保持するオブジェクトであるProcオブジェクトを利用したリファクタが出来る。
Procについてはこちらを参考に。

class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(&formatter) # &を付けてコードブロックを受け取る
    @title = '月次報告'
    @text = ['順調', '最高の調子']
    @formatter = formatter
  end

  def output_report
    @formatter.call(self) #Procオブジェクトの呼び出し
  end
end

# クラスを作成する代わりに、Procオブジェクトに処理をラップする
HTML_FORMATTER = lambda do |context|
  puts "<title>#{context.title}</title>"
  puts '<body>'
  context.text.each do |line|
    puts "<p>#{line}</p>"
  end
  puts '</body>'
end

PLAIN_TEXT_FORMATTER = lambda do |context|
  puts("***#{context.title}***")
  context.text.each do |line|
    puts("***#{line}***")
  end
end

こんな風に、Procベースのストラテジを書くことが出来る。実行する時は、

report = Report.new(&HTML_FORMATTER)
report.output_report

で呼べる。
Procベースのストラテジにすることで、わざわざクラスを作る必要がなくなり、手軽に付け足すことが出来るようになる。しかしもっと重要な利点は、何もないところからメソッドに対してコードブロックを渡すだけでストラテジを作れるようになったこと。すなわち、

report = Report.new { puts 'その場だけの出力' }
report.output_report

このようにコードブロックを直接渡すことでレポートオブジェクトを作ることが出来るようになる。
じゃあ便利だからクラスベースのストラテジじゃなくて全部Procオブジェクトベースでいいかというとそうではない。
上記のようにインターフェイスが単純で、1つのメソッドで事足りるような時のみ、Procベースのストラテジは有効に働く。もしもシンプルなストラテジで要件に合うのなら、Procを使ったやり方にすべき。

要点

  • Strategyパターンは、Template Methodパターンと同様の問題に対する委譲ベースのアプローチによる解。
  • アルゴリズム中の変わる部分を抜き出してサブクラスへと押し込む代わりに、アルゴリズムのパターンごとに、バラバラのオブジェクトとしてシンプルに実装する。
  • 異なるストラテジオブジェクトをコンテキストに対して提供することで、アルゴリズムに多様性をもたらすことが出来る。
  • Strategyパターンに似ているパターンがいくつかある。Strategyパターンでは何かをしてもらいたいコンテキストというオブジェクトがあるが、それをしてもらうにはそのコンテキストへストラテジオブジェクトを提供する必要がある。意図の違いはあるものの、Observerパターンが多くの点で同じように動く。

5章 変更に追従する : Observer

クラスAがあり、それは向こうにあるクラスBで何が起きたか知る必要があるとします。しかし、この2つのクラスを結合させたくはありません。いつまたクラスC(クラスDも!)が現れるかわからないからです。そういう時はObserverパターンを検討しましょう。

Obserberパターンは通知のためのパターン。
結合度を低く保ったまま、観察対象(subject)に変化があった際に自動で観察者(observer)に通知する。

ある人事システムにおいて、誰かの給料が変わった時に経理部門に知らせる必要がある、という場合を考えてみる。今回も駄目な例から。

class Employee
  attr_reader :name, :salary
  attr_accessor :title

  def initialize(name, title, salary, payroll)
    @name = name
    @title = title
    @salary = salary
    @payroll = payroll
  end

  def salary=(new_salary)
    @salary = new_salary
    @payroll.update(self) # 情報を送信
  end
end

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

これで賃金の変更を知らせることが一応できるようになった。

yuji = Employee.new('yuji', 'engineer', 100000, Payroll.new)
yuji.salary = 99999999

このコードの問題は、経理部門への給与の変更通知をハードコーディングしている点。
もしも他のオブジェクト、例えば銀行口座に関係しているクラスに対して財務状態の通知が必要になったら、Employeeクラスを修正しなければならない(実際にはEmployeeに起こった変化など何もない)。
Employeeクラスのプロパティに、Employeeオブジェクトからの最新ニュースを聞くことに関心のあるオブジェクトの一覧(observers)を足すことで、この問題を解決することができる。

class Employee
  attr_reader :name
  attr_accessor :title, :salary

  def initialize(name, title, salary, observers = [])
    @name = name
    @title = title
    @salary = salary
    @observers = observers # コレ
  end

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

  # それぞれのオブザーバーに変更を通知
  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end

  # オブザーバーの追加
  def add_observer(observer)
    @observers << observer
  end

  # オブザーバーの削除
  def delete_observer(observer)
    @observers.delete(observer)
  end
end

給料の変更を受け取りたいオブジェクトならどんなものでも、下記のようにオブザーバとして簡単に登録することができる。

yuji.add_observer(Payroll.new)

Observerパターンを使うことで、EmployeeクラスとPayrollクラスの結合を取り除いている。
他のオブザーバを足すのも簡単。employeeを受け取るupdateメソッドを持っていれば、どのオブジェクトもオブザーバになれる。

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

yuji.add_observer(TaxMan.new)

オブザーバに対する責務を分離する

オブジェクトを観測する必要があるたびに毎回上記のEmployeeクラスのようなコードを書くよりも、もっとよい方法として、モジュールを作ってインクルードする方法がある。オブザーバを管理しているコードを分離することによって、機能的で小さなモジュールに落ち着く。

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

class Employee
  include Subject
  attr_reader :name, :salary

  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

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

実はRubyにはObserverパターンを実装するための便利なObservableモジュールが組み込まれている。このモジュールにより、オブジェクトにObserverパターンを適用するために必要なあらゆるサポートが提供される。使い方も先ほどまでのObserverパターンとほぼ同じ。

require 'observer'
class Employee
  include Observable

  attr_reader :name, :salary

  def initialize(name, salary)
    @name = name
    @salary = salary
  end

  # changedメソッドで、オブジェクトに変更があったかどうかを示すフラグを設定する必要がある。
  # フラグがfalseのままだったら下のnotify_observersは如何なる通知も行わない。
  def salary=(new_salary)
    @salary = new_salary
    changed 
    notify_observers # オブザーバのupdateメソッドを呼ぶ実装がされている
  end
end

yuji.add_observer(Xxx.new) # add_observer()でオブザーバを足すことが出来る。

このようにObservableモジュールはとても便利だが、コードブロックをサポートしてはいない。
コードブロックを渡したい場合、下のように自分で実装することもあるかもしれない。

module Subject
  def initialize
    @observers = []
  end

  def add_observer(&observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.call(self)
    end
  end
end

class Employee
  include Subject
  attr_accessor :name, :salary
  
  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

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

add_observerメソッドでコードブロックを受け取れるようにしたので、コードを単純化できる。オブザーバを追加するにはただadd_observerを呼び出し、コードブロックを渡せばよくなった。

yuji.add_observer do |changed_employee|
  puts "Cut a new check for #{changed_employee.name}"
  puts "His salary id now #{changed_employee.salary}"
end

要点

  • Observerパターンにより、他のコンポーネントの動きを監視するコンポーネントを疎結合に作ることが出来る。
  • ObserverパターンとStrategyパターンは似ている。Observerパターンではオブザーバブルがオブザーバを呼び出し、Strategyではコンテキストがストラテジを呼んでいる。Observerパターンの場合、オブザーバブルオブジェクトで発生しているイベントを他のオブジェクトに伝えている。Strategyパターンの場合は、何かの処理を行うためにそのストラテジオブジェクトを取得する。

6章 部分から全体を組み立てる : Composite

オブジェクトのコレクションをたった1つのオブジェクトのように扱わなければいけない時があります。個々のファイルは削除やコピー、移動させることが出来ますが、ファイルの入ったディレクトリ全体でも、削除やコピー、移動が可能です。オブジェクトのコレクション自体がそこに含まれる個々のオブジェクトのように見える、そんなコレクションが必要なら、きっとCompositeパターンがよいでしょう。

ケーキを作る仕事をしていて、ケーキ1個を作るのに必要な時間を把握したくなったとする。
ケーキの製造のプロセスはとても複雑。生地を作り、生地を型に入れ、オーブンで焼き、焼けたら砂糖をかけ、販売用に箱詰めする。
これら1つずつの工程にかかる時間を把握する必要がある。
そして、例えば生地を作るというプロセス自体も、ケーキ作りと同じように複雑で、色々なタスクで出来上がっている。
つまり、生地を作るというプロセスは、部分であると同時に全体でもある。
GoFは、このように「全体が部分のように振る舞う」という状況を表すデザインパターンを、Compositeパターンと呼んでいる。

【コンポーネント(Component)】
共通のインターフェイスをもつ基底クラス。今回は所要時間を知りたいのでget_time_requiredメソッドを定義している。

class Task
  attr_reader :name

  def initialize(name)
    @name = name
  end

  # 所要時間を返すメソッド
  def get_time_required
    0.0
  end
end

【葉クラス(Leaf)】

プロセスの単純な構成要素で、1つ以上必要。
今回の例では、小麦粉の軽量や卵の豆乳といった単純なものになる。葉クラスも、Componetインターフェイスを実装する。

class AddDryIngredientsTask < Task
  def initialize
    super('Add dry ingredients')
  end

  def get_time_required
    1.0 # 小麦粉と砂糖を加えるのに1分
  end
end

class MixTask < Task
  def initialize
    super('Mix that batter up!')
  end

  def get_time_required
    3.0 # 混ぜるのに3分
  end
end

【コンポジットクラス(Composite)】

コンポジットもコンポーネントだが、サブコンポーネントから作られる、より上位のオブジェクト。
今回の例では、生地の作成やケーキ全体の作成などいくつかの子タスクから構成される複合的なタスクがコンポジットにあたる。

# コンポジットの基底クラス
class CompositeTask < Task
  def initialize(name)
    super(name)
    @sub_tasks = []
  end

  def add_sub_task(task)
    @sub_tasks << task
  end

  def remove_sub_task(task)
    @sub_tasks.delete(task)
  end

  def get_time_required
    time = 0.0
    @sub_tasks.each { |task| time += task.get_time_required }
  end
end

# コンポジットの実クラス
class MakeBatterTask < CompositeTask
  def initialize
    super('Make batter')
    add_subtask(AddDryIngredientsTask.new)
    add_subtask(AddLiquidsTask.new)
    add_subtask(MixTask.new)
  end
end

上のように基底クラスのTaskクラスとCompositeクラスを定義しておけば、あとはコンポーネントを登録していけばいろんなCompositeを作ることができる。
かかる時間を知りたければ、

MakeBatterTask.new.get_time_required

とやるだけで導き出せる。

ツリーを再帰的に探索する

ケーキプロセスにどれたけの数の葉があるか数えるという例を考えてみる。
下のような間違いには要注意。

class CompositeTask < Task
  # 省略

  def total_number_basic_tasks
    @sub_tasks.length
  end
end

上記のコードの間違っている点は、サブタスクもまたサブタスクの集合(コンポジット)である可能性を考慮していない点。
サブタスクが葉でもコンポジットでも対応できるように、再帰的な処理を書く必要がある。

# ベースのコンポーネントに1を返すメソッドを追記
class Task
  # 省略

  def total_number_basic_tasks
    1
  end
end

# 複合タスクには、再帰的に探索する処理を書く
class CompositeTask < Task
  # 省略

  def total_number_basic_tasks
    total = 0
    @sub_tasks.each { |task| total += task.total_number_basic_tasks }
    total
  end
end

このように書けば、total_number_basic_tasksメソッドを呼ぶだけで、そのプロセスの構成タスク数が導き出せる。

要点

  • Compositeパターンは、再帰的な性質を理解できればとても単純なもの。
  • 自然にグループ化して大きなコンポーネントになるオブジェクトをモデル化する必要があり、その複雑なオブジェクトが個々のコンポーネントの特徴を共有している、つまり全体がその部分とよく似ている場合、Compositeパターンがよく合う。
  • Compositeパターンは基本的でありふれたもののため、時々他のパターンに紛れて登場する。例えば15章で扱うInterpreterパターンはCompositeパターンを特化させたもの。

7章 コレクションを操作する : Iterator

オブジェクトのコレクションを隠蔽するコードを書いているのですが、隠し「すぎ」たくないときがあります。クライアントにコレクションのオブジェクトを順番にアクセスさせたいけれど、どこにどうやってそれらのオブジェクトが格納されているかを意識させたくはありません。きっとIteratorパターンがつかえるでしょう。

GoFはIteratorパターンについて、以下のように書いている。

集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する

つまり、Iteratorパターンとは、子オブジェクトのコレクションにアクセスする方法を集約オブジェクトが外部に提供するテクニック。

外部イテレータ

イテレータが集約とは別のオブジェクト。コードを見てみる。

class ArrayIterator
  def initialize(array)
    @array = array
    @index = 0
  end

  def has_next?
    @index < @array.length
  end

  def item
    @array[@index]
  end

  def next_item
    value = @array[@index]
    @index += 1
    value
  end
end

以下のように使用する。

array = ['red', 'green', 'blue']

i = ArrayIterator.new(array)
while i.has_next?
  puts "item: #{i.next_item}"
end

このように、簡単に外部イテレータを作成することができるが、Rubyでは外部イテレータの使用は珍しい。内部イテレータがすでに用意されている。

内部イテレータ

Rubyでいう、eachメソッドなどが内部イテレータに該当する。
内部イテレータではコードブロックを使用することでロジックを集約に伝える。繰り返し動作の全てが集約オブジェクトの「中で」起こるので、内部イテレータと呼ばれている。

def my_each(array)
  i = 0
  while i < array.length
    yield(array[i])
    i += 1
  end
end

Rubyの組み込みメソッドであるeachメソッドは、上記のようにして作ることができる。eachメソッドは「言語組み込み」のループではなく、むしろ内部イテレータの応用といえる。

Enumerable

Enumableをincludeすると、include?メソッドやminメソッドmaxメソッドなど、集約オブジェクトを扱う際に便利なメソッドを追加することができる。
実際に内部イテレータを作ってみる。

class Account
  attr_accessor :name, :balance

  def initialize(name, balance=0)
    @name = name
    @balane = balance
  end

  def <=>(other) # この実装が必要
    balance <=> other.balance
  end
end

class Portfolio
  include Enumerable

  def initialize
    @accounts = []
  end

  def each(&block)
    @accounts.each(&block)
  end

  def add_account(account)
    @accounts << account
  end
end

このようにincludeすることで、便利なメソッドをたくさん使えるようになる。

my_portfolio = Portfolio.new
my_portfolio.add_account(Account.new('ufj', 300000))
my_portfolio.add_account(Account.new('mizuho', 500000))
my_portfolio.any? { |account| account.balance > 400000 } # => true
my_portfolio.all? { |account| account.balance > 400000 } # => false

要点

  • 外部イテレータはコレクションのメンバーを指し示すオブジェクト。ポインタを提供しているとも言える。
  • 内部イテレータはポインタを渡す代わりに、コレクションを扱うためのコードブロックを渡す。
  • Enumerableモジュールをincludeすることで、色々な機能を簡単に追加できる。

8章 命令を実行する : Command

ハガキに書くような指示をラップしたいときがあります。「親愛なるデータベースさん、この手紙を受け取ったなら、行番号7843を削除してください。」ハガキがコードの中でやり取りされることはないでしょうが、Commandパターンはこの状況にうってつけの連絡係です。

Commandパターンとは特定の何かをするための命令の、実行、完了、やり直しなどをするためのパターン。
ユースケースとして、"SlickUI"という新しいGUIフレームワークを構築しているとする。ボタンやアイコンを作って、そのインターフェイスに何かをさせたい。
ユーザーがスクリーン上のボタンをクリックするとon_button_pushメソッドが呼ばれるように、ボタンクラスを作る。

class slickButton
  def on_button_push
    # ボタンが押された時に行うこと
  end
end

現時点ではon_button_pushメソッドの中でどんな処理を行うかは決まっておらず、色んな画面で色んな処理をさせたい。が、以下のように継承で解決をしようとすると拡張性に欠ける。

class SaveButton < SlickButton
  def on_button_push
    # 現在の文書を保存
  end
end

class NewDocumentButton < SlickButton
  def on_button_push
    # 新しい文書を作成
  end
end

ラジオボタンのような他のGUI要素もあることを考えると、これではコーディングがかなりしんどくなる。
そこで、Commandパターンを導入することを考える。非常にシンプルなパターン。コードをみてみる。

class SlickButton
  attr_accessor :command

  def initialize(command) # 作成時に命令を渡しておく
    @command = command
  end

  def on_button_push
    @command.execute if @command
  end
end

class SaveCommand
  def execute
    # 現在の文書を保存
  end
end

動作用のコードをオブジェクトに抜き出して、ボタンクラス(変わらないもの)からボタンを押した時の処理(変わるもの)を分離させる。参照しているコマンドオブジェクトを切り替えれば、そのボタンオブジェクトがもつ命令を簡単に切り替えることもできる。

コードブロックをコマンドとして使う

上記のSaveCommandのexecuteメソッドの中身は、コードブロックとして直接ボタンに渡すこともできる。複雑でない処理の場合は、クラスを作らなくて済むぶんこちらの方がよい。

class SlickButton
  attr_accessor :command

  def initialize(&block)
    @command = block
  end

  def on_button_push
    @command.call if @command
  end
end

new_button = SlickButton.new do
  # 新しい文書を作成
end

記録するコマンド

インストールプログラムを作る場合を考える。ファイルの作成、コピー、移動、削除を行う必要がある。それぞれの処理をコマンドとして構成すれば、簡単にこのような情報の履歴を残しておくことができる。まずは基底クラスの作成から。

class Command
  attr_reader :description

  def initialize(description)
    @description = description
  end

  def execute
  end
end

次に、ファイルの作成、コピー、削除をするコマンドを作る。

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
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
end

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

  def execute
    File.delete(@path)
  end
end

コマンドがたくさんできたので、ここで6章で登場したCompositeパターンを使う。
「コマンドのように動作し、複数の子コマンドをもつクラス」を作成する。

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

  def add_command(cmd)
    @commands << cmd
  end

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

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

これでコマンドの実行記録を出力できるようになった。実際に使ってみる。

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

cmds.execute

上記のようにすればファイルを作成、コピー、削除できる。
記録もされているので、

puts cmds.description

とやれば、

Create file: file1.txt
Copy file: file1.txt to file2
Delete file: file1.txt

と出力される。

コマンドを使ったアンドゥ

元に戻す操作を実装する簡単な方法は、元に戻したい時に単純に変更する前に記憶しておいた状態を復元すること。しかし変更のたびに完全なコピーを作る方法はリソースを浪費してしまう。
こんなときもCommandパターンが役に立つ。execute(実行)メソッドと共に、unexecute(元に戻す)メソッドを追加する。

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 unexecute
    File.delete(@path)
  end
end

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

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

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

最後に、unexecuteメソッドをCompositeCommandクラスにも追加する。

class CompositeCommand < Command
  # 省略

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

イテレートする前にcommands配列を逆順にして、最新のコマンドからunexecuteしていく。

要点

  • Commandパターンでは、ある特定の動作を実行するオブジェクトを構築する。
  • これから行うことのリストや、完了したことのリストを記録する必要がある場合に役に立つ。
  • コマンドが複雑でない場合は、クラスを作らずにコードブロックを渡すことも出来る。
  • CommandパターンとObserverパターンには多くの共通点がある。コマンドもオブザーバも、対象のオブジェクトから呼び出されるオブジェクト。コマンドは何かを行う方法を知っているが、それを実行する対象には興味がない。反対に、オブザーバは呼び出される対象の状態に興味がある。

9章 ギャップを埋める : Adapter

あなたが必要としていることができるオブジェクトがありますが、そのインターフェイスがふさわしくない場合どうすればよいでしょう?このインターフェイスの不整合はとても深くて複雑かもしれませんし、writeをsaveで呼びなおすだけのオブジェクトがあれば十分かもしれません。GoFのオススメはAdapterパターンです

ファイルの暗号化をするEncrypterクラスを作ったとする。
このEncrypterオブジェクトのencryptメソッドで、ファイルではなく文字列を暗号化したい場合を考えてみる。
以下登場人物。

  • Encrypter

ファイルの暗号化をするencryptメソッドを持つ。

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

  def encrypt(reader, writer)
    key_index = 0
    while not reader.eof?
      clear_char = reader.getc
      encrypted_char = clear_char ^ @key[key_index]
      writer.putc encrypted_char
      key_index = (key_index + 1) % @key.size
    end
  end
end

秘密鍵を渡して初期化し、2つの開いたファイルを渡せば暗号化をしてくれる。

encrypter = Encrypter.new('my secret key')
reader = File.open('message.txt')
writer = File.open('message.encrypted', 'w')
encrypter.encrypt(reader, writer)
  • アダプタ

StringIOAdapter。
Encrypterクラスで、ファイルではなく文字列の暗号化をするために、インターフェイス間にある不整合を解消する。

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

  def getc
    raise EOFError if eof?
    ch = @string[@position]
    @position += 1
    return ch
  end

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

StringIOAdapterでEncrypterを使うには、入力ファイルをアダプタに置き換えればよい。

encrypter = Encrypter.new('XYZZY')
reader = StringIOAdaper.new('We attack at dawn')
writer = File.open('out.txt', 'w')
encrypter.encrypt(reader, writer)

上記を見ればわかる通り、StringIOAdapterオブジェクトが必要な処理をすべて持っているので、Encrypterはreaderの中身がファイルか文字列を気にしていない。(今回の場合はgetcメソッドとeof?メソッド)

特異メソッド

アダプタの代替手段として、オブジェクト固有の特異クラスにメソッドを定義する、というものがある。
画面に文字列を表示させるクラスを実装しているとする。

class Renderer

end

Rendererは以下のようなオブジェクトを表示に使う。

class TextObject

end

そこで、以下のような別のよく似たオブジェクトと仕事をさせるにはどうしたらよいだろう?

class BritishTextObject

end

まずはAdapterパターンを使う方法。以下のようにアダプタを作る。

class BritishTextObjectAdapter < TextObject

end

これでRendererはBritishTextObjectと仕事ができるようになった。
しかし、今回のような少しの変更の場合は、特定のインスタンスの振る舞いを変更する方法でも対処できる。
上の例では、BritishTextObjectにtextメソッドとcolorメソッドがないのが問題だが、以下のようにメソッドを与えることができる。

bto = BritishTextObject.new('hello', 'blue')

# 特定のインスタンスのみオーバーライド
def bto.text
  string
end

def bto.color
  colour
end

アダプタを作らずに、簡単に振る舞いを定義している。特定のインスタンスに対してだけオーバーライドしているので、変更は限定的。
以下のような場合は特異メソッドを使うのがよい。

  • 変更内容がシンプル
  • 変更するクラスとその使用方法を理解している

以下のような場合はアダプタを。

  • インターフェイスの不整合が広範囲に及んでいて複雑。
  • そのクラスがどのように動くかわからない。

要点

  • アダプタは必要なインターフェイスと既存のオブジェクトとの間の違いを吸収するためにある。
  • Rubyでは、アダプタを作成する代わりに、オブジェクトを変更して変更を限定的に抑える方法がある。

10章 オブジェクトに代理を立てる : Proxy

正しいオブジェクトがあるけれど、それが遠く、ネットワークのどこか別の場所に離れているとします。しかし、クライアントのコードにそのような場所の問題を気にさせたくはありません。ひょっとすると、出来るだけオブジェクトの生成を遅らせたいかもしれないし、アクセスを制御したいかもしれません。そのような状況には、 Proxyが要るかもしれません。

プロキシーとは代理人のこと。オブジェクトに直接アクセスさせずに間に代理人を立てることで、関心事の分離を行う。

  • 防御プロキシー
  • 仮想プロキシー
  • リモートプロキシー

といった色々な種類のプロキシーがある。

防御プロキシー

銀行口座の状態を維持する簡単なクラスがあったとする。

class BankAccount
  attr_reader :balance

  def initialize(starting_balance)
    @balance = starting_balance
  end

  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    @balance -= amount
  end
end

このクラスへのアクセスを制限する防御プロキシーを作ってみる。
各メソッドの先頭に検査ロジックを追加する。

require 'etc'

class AccountProtectionProxy
  def initialize(real_account, owner_name)
    @subject = real_account
    @owner_name = owner_name
  end

  def deposit(amount)
    check_access
    return @subject.deposit(amount)
  end

  def withdraw(amount)
    check_access
    return @subject.withdraw(amount)
  end

  def balance
    check_access
    return @subject.balance
  end

  def check_access
    if Etc.getlogin != @owner_name # Etcモジュールを使って現在のユーザー名を取得して比較
      raise "Illegal access: #{Etc.getlogin} cannot access account."
    end
  end
end

プロキシーを使ってアクセス制御を行うことで、関心事の分離を上手く行っている。簡単にセキュリティー指針を変更したり、セキュリティを一気に取り除くことができるようになる。

仮想プロキシー

パフォーマンス上の理由から、オブジェクトの生成を本当に必要になるまで遅延させたくなったとする。仮想プロキシーを使った解決方法がある。

class VirtualAccountProxy

  # {BankAccount.new(100)}などのコードブロックを引数として渡す
  def initialize(&creation_block)
    @creation_block = creation_block
  end

  def deposit(amount)
    s = subject
    return s.deposit
  end

  def withdraw(amount)
    s = subject
    return s.withdraw(amount)
  end

  def balance
    s = subject
    return s.balance
  end

  # 銀行口座(subject)がなかったら作るメソッド。各メソッドの先頭に仕込む
  def subject
    @subject ||= @creation_block.call
  end
end

プロキシーが生成されて段階ではまだ銀行口座(subject)は生成されない。
銀行口座のお金の参照や移動が発生した段階で、初めて銀行口座オブジェクトを生成する。
以下のように実行。

# 依存性の注入
# accountは銀行口座のように振る舞えるが、この時点では銀行口座オブジェクトは生成されていない
account = VirtualAccountProxy.new { BankAccount.new(100) }

# この時点で初めて生成
puts account.balance # => 100

銀行口座オブジェクト(subject)が預け入れと引き落としを取り扱い、一方でプロキシーが銀行口座生成時の問題を取り扱う。

リモートプロキシー

銀行口座とは別の、簡単なリモートプロキシーの例を紹介する。
次のコードはRuby固有のSOAPクライアントの仕組みを利用して、気象情報を提供する公開されたSOAPサービスへのプロキシーを作成している。

require 'soap/wdslDriver'

wsdl_url = 'http://www.webservicex.net/WeatherForecast.asmx?WSDL'

proxy = SOAP::WSDLDriverFactory.new(wsdl_url).create_rpc_driver
weather_info = proxy.GetWeatherByZipCode('ZipCode'=>'19128')

サブジェクトが天気予報やドメインに特化したあらゆる事柄に焦点を絞る一方で、プロキシーがデータ通信に集中する。

method_missingを利用したテクニック

銀行口座の例では、プロキシーにdepositメソッドやwithdrawメソッドを書いた(代理人なのでsubjectと同じメソッドを持っていないといけない)。メソッドがもっと増えたらしんどいので、method_missingを利用した別の方法を。
method_missingはObjectクラスに定義されている。なにかしらの定義されていないメソッドを呼ぶと、最終的にObjectクラスのmethod_missinメソッドが呼ばれ、エラー文が吐き出される。これをプロキシ内でオーバライドする。

require 'etc'

class ProtectionProxy
  def initialize(subject, owner_name)
    @subject = subject
    @owner_name = owner_name
  end

  # 引数のnameには呼ぼうとしたメソッド名、*argsには渡した引数が入る
  def method_missing(name, *args)
    check_access
    @subject.send(name, *args) # sendメソッドでsubjectにメッセージを送る
  end

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

こんなにスッキリ書くことができる。仕組みも書いてある通りで非常に単純。メソッドが増えてもなにも書き足す必要はない。
そして何より素晴らしいことに、上のProtectionProxyの中には、Accountが1度も出てこない。銀行口座以外のどんなオブジェクトとも仕事が出来るようになっている。
ProtectionProxyは初期化の際に渡されたオブジェクトがどんなものであろうと、その身代わりをして自分のアクセスポリシーを適用する。
仮想プロキシーも作り直してみる。

class VirtualProxy
  def initialize(&creation_block)
    @creation_block = creation_block
  end

  def method_missing(name, *args)
    subject
    @subject.send(name, *args) # sendメソッドでsubjectにメッセージを送る
  end

  # 銀行口座(subject)がなかったら作るメソッド。各メソッドの先頭に仕込む
  def subject
    @subject ||= @creation_block.call
    @subject
  end
end

この仮想プロキシーも非常に汎用的。
例えば配列の生成を遅延させることが出来る。

array = VirtualProxy.new { Array.new }
array << "xyz" # この時点で初めてArray.newが発動、配列が生成される。

便利だが、注意点もある。

  • method_missingを使うと処理が少しだけ遅くなる。10%ほど。
  • rubyに精通していない人にとっては把握しにくいコードになる。色んな人が見ることが予想される場合はコメントなどで補足する。

要点

  • プロキシーはクライアントとサブジェクトの間に処理を差し込む場所を提供する。
  • method_missingを利用すると、無駄なコードを大量に省ける。また、プロキシーを一般化出来る。
  • ProxyパターンとAdapterパターンはよく似ている。プロキシーは内部のオブジェクトへのアクセスを制御する、アダプタは内部のオブジェクトへのインターフェイスを制御する。次章のデコレータも似たパターン。

11章 オブジェクトを改良する : Decorator

実行時にオブジェクトに責務を追加したい時があります。核となる機能を実装したオブジェクトがあり、時々追加の責務を与えなければいけない場合、きっとDecoratorパターンが利用できます。

既存のオブジェクトに対して簡単に機能を追加するためのパターン。
Decoratorパターンを使うとレイヤ状に機能を積み重ねていくことができ、それぞれの状況で必要なだけの機能を持つオブジェクトを作ることができる。
登場人物はデコレータと具象コンポーネント。

ファイルに何かテキストを書き込む必要があるとする。
そのシステムではプレーンなテキストを書き出したいときもあれば、書き出すテキストに行番号をつけたいときもある。ファイルに書き出す各行にタイムスタンプを含めたいときもある。
まずはダメな例から。

class EnhacedWriter
  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 numbering_write_line(data)
    write_line("#{@line_number}: #{data}")
    @line_number += 1
  end

  def timestamping_write_line(data)
    write_line("#{Time.new}: #{data}")
  end

  def close
    @file.close
  end
end

以下のように普通のテキストを書き出す。

writer = EnhancedWriter.new('out.txt')
writer.write_line('飾り気のない一行') # 普通のテキストを書き出す
writer.numbering_write_line('行番号付きの一行') # 行番号付き
writer.timestamping_write_line('タイムスタンプ付きの一行') # タイムスタンプ付き

これでは、EnhancedWriterを使う全てのクライアントは出力するテキストが行番号付きなのかタイムスタンプ付きなのかそれとも普通のテキストなのか、出力する全ての行で知らなければならない。
また、「行番号とタイムスタンプ付き」といった出力の仕方が出来ない。
解決策として、本当に必要な機能の組み合わせを動的に実行時に組み立てるやり方、つまりDecoratorパターンを使ってみる。
まずは普通のテキストの出力の方法と、その他のファイルに関するいくつかの操作だけを知っている単純なオブジェクト(ConcreteComponent: 具象コンポーネント)を作る。

# GoFでConcreteComponent(具象コンポーネント)と呼ばれている
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

行番号を付けたいときは、デコレータをSimpleWriterとクライアントの間に挿入する。デコレータは、各行に番号を付け足し、全体を元のSimpleWriterに渡す。実際に書き出す処理はSimpleWriterが行う。

# デコレータを何種類も作ることが決まっているので、共通のコードを基底クラスに書き出す
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

インターフェイスが同じなので、クライアントはやり取りをしている相手がSimpleWriterなのかNumberingWriterなのか気にする必要はない。
行番号をつけたければ、以下のようにNumberingWriterにSimpleWriterを注入してやればよい。

writer = NumberingWriter.new(SimpleWriter.new('xxx.txt'))

同じパターンでタイムスタンプを付与するクラスを作ることができる。

class TimeStampingWriter < WriterDecorator
  # 省略

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

「行番号とタイムスタンプ付き」のようなテキストも以下のように簡単に書き出せる。

# 同じインターフェイスを持っているのでこのようにできる
writer = TimeStampingWriter.new(NumberingWriter.new(SimpleWriter.new('xxx.txt')))
writer.write_line('行番号とタイムスタンプ付きの一行')
# '1: 2018-07-15 13:17:53 +0900: 行番号とタイムスタンプ付きの一行' などといった文字列が書き込まれる。

注入するオブジェクトはSimpleWriterでも、SimpleWriterを注入したNumberingWriterでも構わない。ここがミソ。いくつでも繋げて機能を重ねていくことが出来る。
また、当然ながらオプションとしてデコレータ独自の新しいメソッドを追加してもよい。

委譲を簡単に

上記のWriterDecoratorを見てみると、ほとんどのメソッドが次のライターに行を委譲しているだけなのが分かる。このコードは前章のmethod_missingを使って取り除くことが出来るが、Forwardableモジュールを使うのがここでは適している。WriterDecoratorをこのように書き直すことが出来る。

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

method_missingと違い、Forwardableを使うとどのメソッドを委譲させるかを制御することが出来る。
method_missingは大量の呼び出しを全て委譲させたい時に使う。

モジュールを使ったデコレータ

デコレータのクラスをモジュールにリファクタして、extendメソッドを使って動的にmixinすることが出来る。

module NumberingWriter
  attr_reader :line_number

  def write_line(line)
    @line_number ||= 1
    super("#{@line_number}: #{line}")
    @line_number += 1
  end
end

module TimestampWriter
  def write_line(line)
    super("#{Time.new}: #{line}")
  end
end

extendメソッドはあるオブジェクトの継承ツリーのそのクラスの前にモジュールを挿入する。これを利用して、普通のライターから始めて、必要な機能だけを追加していくことが出来る。

writer = SimpleWriter.new('xxx.txt')
writer.extend(NumberringWriter)
writer.extend(TimestampWriter)

writer.write_line('hello')

最後に追加されたモジュールが最初に呼び出される。上の例では、処理はクライアントから、TimestampWriter, NumberingWriter, SimpleWriterの順に進む。
但し、モジュールを使った場合はデコレータを取り除くことが出来ないという欠点があるので、注意が必要。

要点

  • 基本的な機能をカバーする1つのクラスと、それと組みになる一式のデコレータを作ることで、全ての機能を1つで提供するオブジェクトを作る必要がなくなる。
  • デコレータはそれぞれがメソッド呼び出しを受け付け、独自の機能を加え次の並びのコンポーネントに渡す。
  • この本で紹介している「別のオブジェクトの代理オブジェクト」の最後のパターン。1つめのAdapterパターンは不適切なインターフェイスを持ったオブジェクトを正しいインターフェイスのオブジェクトでラップするもの。2つめのProxyパターンはインターフェイスを変えることはせず、内部のオブジェクトへのアクセスを制御する。3つめの基本的なオブジェクトにレイヤ状に機能を追加できるようにするもの。

12章 唯一を保証する : Singleton

クラスのインスタンスがありますが、そのインスタンスがそのクラスの唯一のインスタンスであってほしいとします。つまり、そのインスタンスを全ての人に使わせたいのです。言い換えると、Singletonにしたいようです。

オブジェクトの生成を限定したいときに使うパターン。
あるクラスのインスタンスが1つだけあって、数多くのコードがそのインスタンスにアクセスする必要があるときに、シングルトンを作るとよい。

プログラムの動作を追跡し続ける簡単な機能のロギングクラスがあるとする。
シングルトンでない普通のロギングクラスは次のようになる。

class SimpleLogger
  attr_accessor :level

  ERROR = 1
  WARNING = 2
  INFO = 3

  def initialize
    @log  = File.open("log.txt", "w")
    @level = WANING
  end

  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end
end

ロギングクラスの新しいインスタンスを生成し、そのインスタンスを持ち回りながらロギングクラスを使う。

logger = SimpleLogger.new
logger.level = SimpleLogger::INFO

logger.info('1番目の処理を実行')
# 最初の処理を実行...
logger.info('2番目の処理を実行')
# 次の処理を実行

上記のコードをSingletonパターンを使って書き換える。
まずは唯一のインスタンスを保持するためにクラス変数を追加する。シングルトンインスタンスを返すためのクラスメソッドも追加。

class SimpleLogger

  # 色々省略

  @@instance = SimpleLogger.new

  def self.instance
    return @@instance
  end
end

これで下記の常に同じLoggerオブジェクトを得ることができる。

logger1 = SimpleLogger.instance # Loggerのオブジェクトが返る
logger2 = SimpleLogger.instance # 全く同じオブジェクトが返る

SimpleLogger.instance.info('xxxの処理をしました') # このように使える

現状ではまだ他にオブジェクトを作ることができてしまうため、インスタンスの唯一性が保証されない。そこで、newメソッドをprivateに変更する。

class SimpleLogger
  # 省略...
  @@instance = SimpleLogger.new
  
  def self.instance
    return @@instance
  end

  private_class_method :new # これだけでよい

シングルトンモジュール

上記でシングルトンの実装は一応完了しているが、第2第3のシングルトンを作りたくなった際にさっきまでと同じこと(クラス変数とその変数にアクセスするメソッドを作り、newメソッドをprivateに変更)をしなくてはならず面倒。
Rubyには便利なSingletonモジュールが用意されているので、それをincludeすれば楽に実装出来る。

require 'singleton'

class SimpleLogger
  include Singleton

  # 省略...
end

Singletonモジュールはクラス変数を定義しそれをシングルトンインスタンスで初期化し、instanceというクラスメソッドを作り、newメソッドをprivateに変更する。つまり先ほど自作したシングルトンクラスと全く同様に使うことが出来る。
但し違いもあって、先ほど自作したクラスでは実際に必要になる前にシングルトンインスタンスを生成していたが(eager instantiation: 積極的インスタンス化)、Singletonモジュールでは実際にinstanceメソッドが呼ばれた段階でシングルトンインスタンスを生成する。(lazy instantiation: 遅延インスタンス化)

シングルトンとしてのクラス

先ほど自作したシングルトンでは、インスタンス変数(log,level)とインスタンスメソッド(errorメソッドなど)を定義していたが、それを使用するインスタンスは1つだけなので、クラス変数とクラスメソッドとして定義しておいてもよい。

class ClassBasedLogger
  ERROR = 1
  WARNING = 2
  INFO = 3
  @@log = File.open('log.txt', 'w')
  @@level = WARNING

  def self.error(msg)
    @@log.puts(msg)
    @@log.flush
  end

  def self.warning(msg)
    @@log.puts(msg) if @@level >= WARNING
    @@log.flush
  end

  def self.info(msg)
    @@log.puts(msg) if @@level >= INFO
    @@log.flush
  end

  def self.level=(new_level)
    @@level = new_level
  end

  def self.level
    @@level
  end
end

以下のように使える。

ClassBasedLogger.level = ClassBasedLogger::INFO

ClassBasedLogger.info('xxxの処理をしました')

クラスベースのシングルトンにすることによって、2つめのインスタンスが作られないことに確信を持てる。

シングルトンとしてのモジュール

モジュールレベルメソッドと変数を、クラスレベルにおける方法と全く同じように定義することが出来る。

module ModuleBasedLogger
  ERROR = 1
  WARNING = 2
  INFO = 3
  @@log = File.open('log.txt', 'w')
  @@level = WARNING

  def self.error(msg)
    @@log.puts(msg)
    @@log.flush
  end

  # 省略...
end

モジュールはインスタンス化出来ないので、メソッドの入れ物としてだけ存在していることが明示的になる。

使用上の注意

  • 状態を持たないように。シングルトンをグローバル変数のように使ってはならない。
  • シングルトンを作ろうと思ったら、それが本当に唯一存在すべきものなのか、そして簡単にアクセス出来るという特徴を与えられるべきかを確認する。作りすぎない。

テストの対処法

Singletonパターンの厄介事の1つに、単体テストがやりにくいというものがある。
よい単体テストは他のテストから独立している必要がある。複数のテストでシングルトンをテストしていると、それぞれのテストがシングルトンを変更してしまうことがある。これではテストの順番に依存してしまうことになる。以下のように、2つのクラスを作る対処法がある。

require 'singleton'

class SimpleLogger
  # ロガーの機能は全てこのクラスが持つ
end

class SingletonLogger < SimpleLogger
  include Singleton
end

実際はSingletonLoggerを使い、テストでは非シングルトンのSimpleLoggerクラスを使用する。

要点

  • シングルトンクラスには1つだけインスタンスしかなく、アクセスはどこからでも行える。
  • クラスメソッドやモジュールを使って実装も出来る。
  • シングルトンを使うと、コードの結合度が高まりやすいので気をつける。
  • 嫌われている理由の1つにテストのしづらさがあるが、クラスを2つ作る対処法がある。

13章 正しいクラスを選び出す : Factory

基底クラスを書いているとします。つまり、拡張されることを意図しています。あなたが基底クラスから離れて幸せにコーディングしている際、新しいオブジェクトを作る必要があり、そのオブジェクトの種類をちゃんと知っているのはサブクラスだけだと気がつきました。そんな時はFactory Methodが必要かもしれません。

矛盾のない一連のオブジェクトを作るにはどうすれば良いでしょう?たくさんの種類の自動車をモデル化するシステムがあるとします。しかし、全てのエンジンが全ての燃料や冷却システムと互換性があるわけではありません。あなたの自動車モデルがフランケンシュタインの怪物のようにはならないと確信を持つためにはどうしますか?一連の全てのオブジェクトを作ることに専念するクラスを作ることでしょう。それはAbstract Factoryと呼ばれます。

オブジェクトの生成とオブジェクトの使用の結合度を下げるためのパターン。
工場であるファクトリはオブジェクトの生成のみを担う。
専門家のファクトリに任せることで、作成者(Creator)はオブジェクトの生成手順や種類を意識せずに使用することが出来るようになる。
Factory MethodパターンとAbstract Factoryパターンの2種に分けて説明されている。

Factory Method

池の生息環境のシミュレーションの作成を依頼されたとする。サービスの成長と共に少しずつリファクタしていきつつ説明をする。
まずはアヒルと池だけのケース。

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    puts "アヒルの#{name}は食事中です。"
  end

  def sleep
    puts "アヒルの#{name}は寝ました"
  end
end

class Pond
  def initialize(duck)
    @duck = duck
  end

  def simulate_one_day
    @duck.eat
    @duck.sleep
  end
end

下記のように池の1日をシミュレーションする。

pond = Pond.new(Duck.new("アヒル1"))
pond.simulate_one_day

サービスが高評価を得て、カエルもモデル化する必要が出来たとする。
duckと結合してしまっている上記の池クラスをFactory Methodパターンを使ってリファクタしてみる。

class Pond
  def initialize(animal)
    @animal = new_animal
  end

  def simulate_one_day
    @animal.eat
    @animal.sleep
  end
end

class DuckPond < Pond
  def new_animal(name)
    Duck.new(name)
  end
end

class FrogPond < Pond
  def new_animal(name)
    Frog.new(name)
  end
end

これで正しい池を選ぶだけでよくなった。

pond = FrogPond.new("カエルA")
pond.simulate_one_day

このようにクラスの選択の決定をサブクラスに押し付けるテクニックを、GoFではFactory Methodパターンと呼んでいる。
2つの別々のクラス階層があり、それぞれCreator(作成者)とProduct(製品)と呼ぶ。
上記の例では、作成者はPondクラスで、製品がDuckクラス。また、DuckPondは具象作成者(ConcreteCreator)。
見てわかる通り、3章のTemplate Methodパターンを、新しいオブジェクト作成の問題に適用しただけのパターンである。

パラメータ化されたファクトリメソッド

さて、さらにサービスが育ったとして、今度は植物について同様にシミュレートすることになったとする。
パラメータ化されたファクトリメソッドを使って、引数で渡されたシンボルによって動物と植物のどちらも作り出すことが出来るようにリファクタしてみる。

class Pond
  def initialize(:animal, :plant)
    @animal = new_organism(:animal, "動物A")
    @plant = new_organism(:plant, "植物A")
  end
end

class WaterLily
  def initialize(name)
    @name = name
  end

  def grow
    puts "スイレンの#{name}は浮きながら日光を浴びて育ちます。"
  end
end

# Duckクラスは省略

class DuckWaterLilyPond < Pond
  def new_organism(type, name)
    if type == :animal
      Duck.new(name)
    elsif type == :plant
      WaterLily.new(name)
    else
      raise "Unknown organism type: #{type}"
    end
  end
end

パラメータ化されてファクトリメソッドを使うと、サブクラスが定義するファクトリメソッドが1つだけになるため、コードを小さく出来る。

オブジェクトとしてのクラス

ただ、ここまでのFactory Methodパターンには、オブジェクトの型ごとに別々のサブクラスを必要とするという重大な欠点がある。上記のDuckWaterLilyPondというクラス名にも表れているが、アヒルとスイレン以外の動植物が少し増えるだけで、作るクラスの量は莫大になってしまう。
ここで、Rubyにおいてはクラスがただのオブジェクトであることを利用して、クラスをインスタンス変数に格納するやり方で、問題を解決してみる。

class Pond
  def initialize(animal_class ,plant_class)
    @animal_class = animal_class
    @plant_class = plant_class
    @animal = new_organism(:animal, "動物A")
    @plant = new_organism(:plant, "植物A")
  end

  def simulate_one_day
    @animal.eat
    @animal.sleep
    @plant.grow
  end

  def new_organism(type, name)
    if type == :animal
      @animal_class.new(name)
    elsif type == :plant
      @plant_class.new(name)
    else
      raise "Unknown organism type: #{type}"
    end
  end
end

以下のようにクラスを渡せばよい。

pond = Pond.new(Duck, WaterLily)
pond.simulate_one_day

さて、ここでさらにサービスが大成功して、池以外の生息地のタイプをモデル化出来るように拡張しなければならなくなったとする。まずはジャングルをシミュレートすることになった。
とりあえず、ジャングルの動植物を作る。

class Tree
  def initialize(name)
    @name = name
  end

  def grow
    puts "樹木の#{@name}が高く育っています。"
  end
end

class Tiger
  def initialize(name)
    @name = name
  end

  def eat
    puts "トラの#{@name}は食べたいものを何でも食べます。"
  end

  def sleep
    puts "トラの#{@name}は眠くなったら眠ります。"
  end
end

Pondクラスの名前は一般化して、生息環境(Habitat)クラスに変更しておく。

jungle = Habitat.new(Tiger, Tree)
jubgle.simulate_one_day

pond = Habitat.new(duck, WaterLily)
pond.simulate_one_day

このように、今までのPondクラスと同じようにHabitatクラスを使うことが出来るが、問題点もある。

Abstract Factory

新しく作ったHabitatクラスの問題点は、動植物のチグハグな(生物学的にありえない)組み合わせを作ることが出来てしまうこと。この問題を解決するために、Habitatにここの動植物のクラスを渡す代わりに、辻褄の合う製品の組み合わせの作り方を知っているオブジェクトを1つだけ渡すようにする。
矛盾のないオブジェクトの組み合わせを作るためのこのオブジェクトは、アブストラクトファクトリと呼ばれる。Abstract FactoryパターンはGoFのパターンの1つ。
次のコードは、1つはジャングル用、もう1つは池用のアブストラクトファクトリ。

class PondOrganismFactory
  def new_animal(name)
    Duck.new(name)
  end

  def new_plant(name)
    WaterLily.new(name)
  end
end

class JungleOrganismFactory
  def new_animal(name)
    Tiger.new(name)
  end

  def new_plant(name)
    Tree.new(name)
  end
end

あとはHabitatのinitializeを少し修正すれば使えるようになる。

class Habitat
  def initialize(organism_factory)
    @organism_factory = organism_factory
    @animal = @organism_factory.new_animal("動物A")
    @plant = @organism_factory.new_plant("植物A")
  end

  # 以下略...
end

以下のように使う。

jungle = Habitat.new(JungleOrganismFactory.new)
jungle.simulate_one_day

pond = Habitat.new(PondOrganismFactory.new)
pond.simulate_one_day

Factory MethodパターンがTemplate Methodパターンを利用したのに対し、Abstract FactoryパターンはStrategyパターンを利用している。

オブジェクトとしてのクラスその2

上記の例では、アブストラクトファクトリをジャングル用と池用の2つ作る必要があったが、Rubyの能力を活かして一般化のリファクタが出来る。

class OrganismFactory
  def initialize(animal_class, plant_class)
    @animal_class = animal_class
    @plant_class = plant_class
  end

  def new_animal(name)
    @animal_class.new(name)
  end

  def new_plant(name)
    @plant_class.new(name)
  end
end
jungle_organism_factory = OrganismFactory.new(Tree, Tiger)
jungle = Habitat.new(jungle_organism_factory)
jungle.simulate_one_day

要点

  • Factory MethodパターンはTemplate Methodパターンをオブジェクトの生成に応用したもの。クラスの選択をサブクラスに任せる。
  • Abstract Factoryパターンは矛盾のないオブジェクトの組み合わせを作りたいときに使う。
  • どちらのパターンもRubyの力で単純化出来る。
  • 次に取り上げるBuilderパターンも新しいオブジェクトの生成のためのパターン。Factoryパターンのように正しいクラスを選ぶことより、複雑なオブジェクトを構築することに焦点を合わせたもの。

14章 オブジェクトを組み立てやすくする : Builder

とても複雑で、組み立てるのに特別なコードが要求されるオブジェクトを作ることがあります。さらに悪いことに、組み立てのプロセスは状況によって変える必要があります。そんな時はBuilderパターンが必要です。

オブジェクト構築のロジック自体をあるクラスにカプセル化する。
カプセル化することで構築を楽に出来るようになるだけでなく、その実装の詳細を隠蔽する。

コンピュータを製造するためのシステムを作っているとする。
話を簡単にするために、コンピュータはディスプレイとマザーボード、そしていくつかのドライブから構成されているとする。

class Computer
  attr_accessor :display, :motherboard
  attr_reader :drives

  def initialize(display=:crt, motherboard=Motherboard.new, drives=[])
    @display = display
    @motherboard = motherboard
    @drives = drives
  end
end

ディスプレイは:crtか:lcdのどちらかとする。マザーボードはそれ自体が複数の部品で構成されている。

class CPU
  # CPU
end

class BasicCPU < CPU
  # あまり高速ではないCPUについてのたくさんのコード
end

class TurboCPU < CPU
  # 高速のCPUについてのたくさんのコード
end

class Motherboard
  attr_accessor :cpu, :memory_size
  def initialize(cpu=BasicCPU.new, memory_size=1000)
    @cpu = cpu
    @memory_size = memory_size
  end
end

ドライブにはハードディスクドライブとCDとDVDの3種類がある。

class Drive
  attr_reader :type # :hard_diskか:cdか:dvd
  attr_reader :size # 単位はMB
  attr_reader :writable # ドライブが書き込み可能ならばtrue

  def initialize(type, size, writable)
    @type = type
    @size = size
    @writable = writable
  end
end

これだけ単純化しても組み立てはなかなか面倒。

motherboard = Motherboard.new(TurboCPU.new, 4000)

drives = []
drives << Drive.new(:hard_drive, 200000, true)
drives << Drive.new(:cd, 760, true)
drives << Drive.new(:dvd, 4700, false)

computer = Computer.new(:lcd, motherboard, drives)

Builderパターンの考え方は、このような構築のロジック自体をあるクラスにカプセル化するというもの。
コンピュータのビルダは以下のようになる。

class ComputerBuilder
  attr_reader :computer

  def initialize
    @computer = Computer.new
  end

  def turbo(has_turbo_cpu=true)
    @computer.motherboard.cpu = TurboCPU.new
  end

  def display=(display)
    @computer.display
  end

  def memory_size=(size_in_mb)
    @computer.motherboard.memory_size = size_in_mb
  end

  def add_cd(writable=false)
    @computer.drives << Drive.new(:cd, 760, writable)
  end

  def add_dvd(writer=false)
    @computer.drives << Drive.new(:dvd, 4000, writable)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << Drive.new(:hard_disk, size_in_mb, true)
  end
end

ComputerBuilderクラスはComputerのインスタンスを作るための全ての詳細を分離する。
これを使うには、ビルダの新しいインスタンスを作成し、コンピュータに必要な全てのオプションを指定するプロセスを1つずつ行う。

builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(1000000)

computer = builder.computer

GoFではビルダオブジェクトのクライアントをディレクタ(director)と呼び、作られるオブジェクトは製品(product)と呼ぶ。
ビルダは製品を作る負荷を軽減するだけでなく、その実装の詳細を隠蔽する役割を持つ。
コードを見てもわかる通り、ディレクタはDVDやハードディスクを表すクラスについての詳細を知る必要がない。

妥当性の保証

ビルダでオブジェクトの生成を簡単にするだけでなく、より安全にすることが出来る。
例えば以下のように(attr_readerで生成された)computerメソッドを拡張して、ハードウェア構成の妥当性を確実にすることが出来る。

def computer
  raise "Not enough memory" if @computer.motherboard.memory_size < 250
  raise "Too many drives" if @computer.drives.size > 4
  hard_disk = @computer.drives.find{ |drive| drive.type == :hard_disk }
  raise "No hard disk" unless hard_disk
  @computer
end

マジックメソッドを使ったビルダ

だいぶ改善はされたが依然としてcomputer作成にはたくさんのメソッドを呼ばなければならない。
より簡潔にする方法として、特定のパターンに従ったメソッド名を組み立ててそれをビルダに適用する、というものがある。
例えば、新しいコンピュータを次のように構成する。

builder.add_turbo_and_dvd_and_harddisk

このようにメソッド名をandとアンダーバーで区切ってビルダに送るという命名規則を決めておいて、10章で出てきたようにビルダ内でmethod_missingをオーバーライドする。

def method_missing(name, *args)
  words = name.to_s.split('_')
  return super(name, *args) unless words.shift == 'add'
  words.each do |word|
    next if word == 'and'
    add_cd if word == 'cd'
    add_dvd if word == 'dvd'
    add_hard_disk(100000) if word == hard_disk
    turbo if word == 'turbo'
  end
end

ビルダ以外であっても、クライアントのコードで複数のオプションを簡潔に記述させたい場合に、マジックメソッドのテクニックはいつでも利用できる。

要点

  • Builderパターンの考え方は、オブジェクトを作り出すのが難しい場合や、オブジェクトを構成するのに大量のコードを書かなければいけない場合に、それらの生成のためのコードを別のクラスであるビルダに分離するというもの。
  • ビルダはオブジェクトの構成を制御しているので、無効なオブジェクトを作ることを防止することが出来る。
  • method_missingを使って複数のメソッドを1度に呼び出すことが出来る。

15章 専用の言語で組み立てる : Interpreter

問題を解決するために間違ったプログラミング言語を使っている感覚を持ったことはありませんか?正気ではないと思われるかもしれませんが、そういう時は直接その問題に当たるのをやめて、より簡単に問題を解決する言語のためのInterpreterを作るべきかもしれません。

Interpreterとは、問題解決に特化した専用言語を構築するためのパターン。
専用言語で書かれたプログラムはパーサで抽象構文木(AST: Abstract Syntax Tree)というオブジェクトのツリー構造に変換され、解釈(interpret)される。
また、パターンの鍵となるメソッドはinterpret、またはevaluateやexecuteと名付けられる。

ユースケースとして、様々な形式やサイズのファイルを管理するプログラムを作ることになった場合を考える。
このプログラムでは、MP3ファイルや、書き込み可能ファイルといったような、ある特定の特徴を持ったファイルを頻繁に検索する必要がある。
また、「MP3ファイルで容量が大きい」ものや、「JPEGファイルで読み取り専用」といったような、ある特定の特徴の組み合わせを持つファイルを検索する必要もある。
まずは単純に全てのファイルを返す、基本的なファイル検索クラスを作る。

require 'find'

class Expression
  # 共通の処理はここに...
end

class All < Expression
  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p
    end
    results
  end
end

以下のように使う。

expr_all = All.new
files = expr_all.evaluate('test_dir')

evaluateメソッドで、ファイルを再帰的に検索して返す。
次に、与えられたパターンと名前がマッチする全てのファイルを返すクラスを作る。

class FileName < Expression
  def initialize(pattern)
    @pattern = pattern
  end

  def evaluate(dir) # このdirをGoFではコンテキストと呼ぶ
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      name = File.basename(p) # basenameメソッドはファイル名を返す
      results << p if File.fnmatch(@pattern, name)
    end
  end
end

MP3ファイルだけが欲しい場合に以下のように使う。

expr_mp3 = FileName.new('*.mp3')
mp3s = expr_mp3.evaluate('test_dir')

同様にして、大きなファイルや書き込み可能なファイルの検索用クラスを以下のように作成出来る。

class Bigger < Expression
  def initialize(size)
    @size = size
  end

  def evaluate(dir)
    results = []
    Find.find8(dir) do |p|
      next unless File.file?(p)
      results << if File.size(p) > @size
    end
  end
end

class Writable < Expression
  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p if File.writable?(p)
    end
  end
end

すでに作成済みの4つの検索方法を組み合わせて複雑な検索を実現させてみる。
まずはNot検索から。
新しいクラスを作らずに、例えば書き込み可能でないものだけの検索をする場合。

class Not < Expression
  def initialize(expression)
    @expression = expression
  end

  def evaluate(dir)
    # 配列同士の引き算をして、Notを実現
    All.new.evaluate(dir) - @expression.evaluate(dir)
  end
end 

上記のようにNotクラスを作っておいて、

expr_not_writable = Not.new(Writable.new)
readonly_files = expr_not_writable.evaluate('test_dir')

このようにDIのテクニックを使い、Notクラスはどの検索条件とも仕事をすることが出来る。
同様に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 # ORなのでどちらかに入っているものを返す
  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).sort.uniq # ANDなのでどちらにも入っているものを返す
  end
end

これで、複雑なファイル検索ができるようになった。
例えば書き込み可能でない大きなMP3ファイルは以下のように検索する。

complex_expression = And.new(
                        And.new(Bigger.new(1024), 
                                FileName.new('*.mp3')),
                        Not.new(Writable.new))

complex_expression.evaluate('test_dir')
complex_expression.evaluate('/tmp') # 複数のコンテキストで繰り返し使える

ファイル検索の機能はしっかり実装できているが、And,Or,Notがかなりごちゃごちゃしている。
以下のように拡張することで、インタープリタを楽に使ってもらえる。

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

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

このような演算子の定義により、以前は

Or.new(
  And.new(Bigger.new(2000), Not.new(Writable.new)),
  FileName.new('*.mp3'))

とごちゃごちゃしていたものを、

(Bigger.new(2000) & Not.new(Writable.new)) | fileName.new('*.mp3')

という風に、ファイル検索式を縮めて書けるようになった。

要点

  • Interpreterパターンは一連の式として新しい小さな言語を考え、それらの式を木構造に分解する。
  • Interpreterパターンは、柔軟性と拡張性というメリットをもたらす。別々のASTを作ることで、同じインタープリタクラスに別々のことをさせることが出来る。
  • インタープリタを使うと処理速度が遅くなる傾向があるので、高い性能を必要としない領域に制限するのがよい。

16章 オリジナルの言語を作る : Domain-Specific Language (DSL : ドメイン特化言語)

専門分野に特化した小さな言語を作る動的な仕組みである。
バックアッププログラムを作ることを考える。
このプログラムは頻繁に起動され、大事なファイルを安全なディレクトリにコピーする。
このプログラムをDSLを使って作成することにした。
ユーザーが例えば以下のように普通の言葉で対話するように使える言語にしたい。

backup.pr
backup '/home/documents'
backup '/home/music', file_name('*.mp3') & file_name('*.wav')
backup '/home/images', except(file_name('*.tmp'))
to '/external_drive/backups'

interbal 60

このように書くと、60分に1度external_drive/backupsディレクトリにバックアップを出来るようにしたい。つまり、新しいルールの言語を作りたい。ファイル検索には15章で作ったインタープリタを使うことにする。
まずはevalメソッドの説明から。

require 'finder'

# 仮のbackupメソッド
def backup(dir, find_expression=All.new)
  puts "Backup called, source dir=#{dir} find expr=#{find_expression}"
end

# 仮のtoメソッド
def to(backup_directory)
  puts "To called, backup dir=#{backup_directory}"
end

# 仮のintervalメソッド
def interval(minutes)
  puts "Interval called, interval = #{minutes} minutes"
end

# 以下のようにevalメソッドを利用すると、backup.prの内容を読み込み、Rubyプログラムとして実行してくれる
# backupメソッドが3回と、toメソッドとinterbalメソッドが呼ばれる
eval(File.read('backup.pr'))

evalメソッドによって命令文をDSLの中に取り込んでそれをRubyプログラムとして解釈している。
つまり、ユーザーは知らず知らずのうちにRubyのメソッド呼び出しを書いていることに。
では、実際の処理内容を記述していく。

class Backup
  include Singleton # 常に1つだけあればよいので、シングルトンにする

  attr_accessor :backup_directory, :interval
  attr_reader :data_sources

  def initialize
    @data_sources = []
    @backup_directory = '/backup'
    @interval = 60
  end

  def backup_files
    this_backup_dir = Time.new.ctime.tr(' :', '_')
    this_backup_path = File.join(@backup_directory, this_backup_dir)
    @data_sources.each { |source| source.backup(this_backup_path) }
  end

  # コピーと停止を繰り返す
  def run
    while true
      backup_files
      sleep(@interval * 60)
    end
  end
end

class DataSource
  attr_reader :directory, :finder_expression

  def initialize(directory, finder_expression)
    @directory = directory
    @finder_expression = finder_expression
  end

  def backup(backup_directory)
    files = @finder_expression.evaluate(@directory)
    files.each do |file|
      backup_file(file, backup_directory)
    end
  end

  def backup_file(path, backup_directory)
    copy_path = File.join(backup_directory)
    FileUtils.mkdir_p(File.dirname(copy_path))
    FileUtils.cp(path, copy_path)
  end
end

準備が出来たので、backup,to,intervalを上記のクラスに合わせて書き換えて、実際に使ってみる。

def backup(dir, find_expression=All.new)
  Backup.instance.data_sources << DataSource.new(dir, find_expression)
end

def to(backup_directory)
  Backup.instance.backup_directory = backup_directory
end

def interval(minutes)
  Backup.instance.interval = minutes
end

eval(File.read(''backup.pr))
Backup.instance.run

Backupクラス、DataSourceクラスとデータ構造を定義し、DSL言語に対応するトップレベルメソッドを設定している。
最後の行でバックアップのサイクルが開始している。

要点

  • rubyの柔軟な構文と組み合わせることによって、より少ないコードで目的を達成する内部DSL(= 汎用のプログラミング言語の書き方を工夫して、見かけ上の構文を自然言語に近づけた言語)を定義することが出来る。
  • evalメソッドなどを使い、DSLプログラムを実行する。
  • 内部DSLの本質は、誰かが書いた任意のコードをプログラムに取り込むということ。セキュリティが問題となる場合は使用を避ける。

17章 カスタムオブジェクトを作る : メタプログラミング

Rubyでは、すでに存在するクラスに変更を加えたり、クラスとは無関係にオブジェクトの振る舞いを変えることができる。その柔軟性を活用して必要なクラスやオブジェクトを実行時に動的に作るやり方がある。

13章で見た野生生物の生息環境についてのシミュレーションにもっと柔軟性を持たせることを考える。
まずは準備運動として、生息環境にあった植物を産み出すメソッド作ってみる。適したクラスを選ぶのではなく、その場で必要なオブジェクトを作る。

# 生息環境に合った返り値を持つメソッドを持つオブジェクトを返すメソッド
def new_plant(stem_type, leaf_type)
  plant = Object.new

  if stem_type == :fleshy
    def plant.stem
      'fleshy'
    end
  else
    def plant.stem
      'woody'
    end
  end

  if leaf_type == :broad
    def plant.leaf
      'broad'
    end
  else
    def plant.leaf
      'needle'
    end
  end

  plant
end

呼び出し元から渡される選択肢に応じて特異メソッドのleafメソッドとstemメソッドを追加する。結果として、独自のオブジェクトが作られている。
同じことをモジュールで行うことも出来る。例えば肉食動物と草食動物で別々のモジュールを用意する。

module Carnivore
  def diet
    'meat'
  end

  def teeth
    'sharp'
  end
end

module Herbivore
  def diet
    'plant'
  end

  def teeth
    'flat'
  end
end

同様に、日中に活動する動物と夜行性の動物の2つのモジュールを作る。

module Diurnal
  def sleep_time
    'night'
  end

  def awake_time
    'day'
  end
end

module Nocturnal
  def sleep_time
    'day'
  end

  def awake_time
    'night'
  end
end

モジュールを利用して新しいオブジェクトを作るには、以下のようにする。

def new_animal(diet, awake)
  animal = Object.new

  if diet == :meat
    animal.extend(Carnivore)
  else
    animal.extend(Herbivore)
  end

  if awake == :day
    animal.extend(Diurnal)
  else
    animal.extend(Nocturnal)
  end

  animal
end

extendメソッドでモジュールを渡すことにより、オブジェクトに特異メソッドを追加している。

さて、生息環境シミュレータに新しい2つの要求が出てきたとする。
1つめは、ある地域に生息する生き物でグループ化をすること。例えばジャングルに生息しているグループとして、トラと樹木をグルーピングしたい。
2つめは、全ての生物に対して生物学的な分類ができるようにコードを追加すること。例えば、トラはP.tigris種であり、ヒョウ属、ネコ科に属し、最終的には動物界に分類される。
この2点を解決するために、生息地域で生物を組織化することと、生物学上の分類で組織化する必要がある。これにはCompositeパターンが使える。
まずは下記のような感じで、クラス内で、ある生息地域に属していることや、ある生物学上の分類に属していることを宣言する。

# member_ofは後ほど定義
class Tiger < CompositeBase
  member_of(:population)
  member_of(:classification)

  # 省略...
end

class Tree < CompositeBase
  member_of(:population)
  member_of(:classification)

  # 省略...
end

ここで言わんとしていることは、TigerクラスのインスタンスもTreeクラスのインスタンスも、ある生息地域のグループに属していると同時に、ある生物学上の分類のグループに属しているということ。
つまり、同時に異なるコンポジットのリーフノードであるということ。
また、種を表すクラスや生息地域を表すクラスもコンポジットとして宣言できる必要がある。

# composite_ofは後ほど定義
class Jungle < Composite
  composite_of(:population)

  # 省略...
end

class Species < CompositeBase
  composite_of(:classification)

  # 省略...
end

例えばトラの一種を作って、あるジャングルに生息させることが簡単に出来るようになった。

tony_tiger = Tiger.new('tony')
se_jungle = Jungle.new('southeastern jungle')
se_jungle.add_sub_population(tony_tiger) # メソッドは後ほど動的に定義

tony_tiger.parent_population # => 'southeastern jungle'

また、生物学上の分類でも、以下のように同じことが出来るようにしたい。

species = Species.new('P. tigris')
species.add_sub_classification(tony_tiger)

tony_tiger.parent_classification # => 'P. tigris'

以上のような振る舞いを動的に獲得させるには、次のようにCompositeBaseクラスを作ればよい。

class CompositeBase
  attr_reader :name 

  def initialize(name)
    @name = name
  end

  def self.member_of(composite_name)
    code = %Q{attr_accessor :parent_#{composite_name}}
    class_eval(code)
  end

  def self.composite_of(composite_name)
    member_of(composite_name)

    code = %Q{
      def sub_#{composite_name}s
        @sub_#{composite_name}s = [] unless @sub_#{composite_name}s
        @sub_#{composite_name}s
      end

      def add_sub_#{composite_name}(child)
        return if sub_#{composite_name}s.include?(child)
        sub_#{composite_name}s << child
        child.parent_#{composite_name} = self
      end
      
      def delete_sub_#{composite_name}(child)
        return unless sub_#{composite_name}s.include?(child)
        sub_#{composite_name}s.delete(child)
        child.parent_#{composite_name} = nil
      end
    }
    class_eval(code)
  end
end

%Qで文字列を作り、class_evalメソッドを使いRubyのコードとして評価している。やっていることは実はシンプル。

要点

  • rubyの動的な性質を使うことで、オブジェクトを生成した後に、メソッドを1つずつもしくはモジュール全体を追加することが出来る。
  • class_evalを使うことで、実行時に完全に新しいメソッドを生成することが出来る。
  • メタプログラミングを使うと、実際のプログラムの動きとコードの記述が離れる。つまりデバッグがしにくくなりがちなので、使いどころは考える。また、テストは必須。

18章 Convention over Configuration(設定より規約)

Railsフレームワークからのパターン。
設定の負荷を軽減するためのアイデア。

メッセージゲートウェイの構築を依頼されたとする。
メッセージを受け取りそれを最終的な宛先に送るプログラムを作る。
メッセージは以下のようになっている。

require 'uri'

class Message
  attr_accessor :from, :to, :body

  def initialize(from, to, body)
    @form = from
    @to = URI.parse(to)
    @body = body
  end
end

toに入るURIには以下の3つの形式が予想される。

# email
smtp://yuji@example.com
# HTTP POST
http://example.com/some/place
# ファイル
file:///home/messages/message84.txt

メッセージゲートウェイの鍵となるのは、新しいプロトコルを追加するのが簡単でなければならないということ。例えば、FTP経由でメッセージを送信することが必要になった場合に、それに対応するための拡張は簡単でなければならない。
拡張性を念頭に置きつつ、まずは上記3つの異なる形式のメッセージを取り扱うことを考える。これにはAdapterパターンが適している。

# email
require 'net/smtp'

class SmtpAdapter
  MailServerHost = 'localhost'
  MailServerPort = 25

  def send_message(message)
    from_address = message.from.user + '@' + message.from.host
    to_address = message.to.user + '@' + message.to.host
    
    email_text = "From: #{from_address}\n"
    email_text += "To: #{to_address}\n"
    email_text += "Subject: Forwarded message\n"
    email_text += "\n"
    email_text += "message.body"

    Net::SMTP.start(MailServerHost, MailServerPort) do |smtp|
      smtp.send_message(email_text, from_address, to_address)
    end
  end
end
# HTTP経由
require 'net/http'

class HttpAdapter
  def send_message(message)
    Net::HTTP.start(message.to.host, message.to.port) do |http|
      http.post(message.to.path, message.body)
    end
  end
end
# ファイル
class FileAdapter
  def send_message(message)
    # URLからパスを取得し、先頭の'/'を取り除く
    to_path = message.to.path
    to_path.slice!(0)

    File.open(to_path, 'w') do |f|
      f.write(message.body)
    end
  end
end

次に適切なアダプタを選択することを考えるが、以下のようにしては拡張が大変なのでイケてない。

def adapter_for(message)
  return SmtpAdapter.new if protocol == 'smtp'
  return HttpAdapter.new if protocol == 'http'
  return FileAdapter.new if protocol == 'file'
  nil
end

アダプタを追加したい人が、アダプタクラスを作成するだけでなく、システムに新しいアダプタを認識させるために何かしないといけない状況を打破したい。
そこで、 Convention over Configuration(設定より規約)の考え方を使う。
アダプタを作る際に、「アダプタクラスは、Protocol + Adapterと命名すること」という規約を作れば、システムは以下のようにアダプタクラスをその名前に基づいて選択することができる。

def adapter_for(message)
  protocol = message.to.scheme.downcase
  adapter_name = "#{protocol.capitalize}Adapter"
  adapter_class = self.class.const_get(adapter_name)
  adapter_class.new
end

次に、クラスのロードに関する問題を解決する。
アダプタクラスをRubyインタープリタにロードするために、アダプタクラスが定義されたファイルをrequireする必要がある。

require 'smtp_adapter'
require 'http_adapter'
require 'file_adapter'

このようにアダプタのrequireをハードコーディングすると、アダプタ作成の際にrequire文も付け足さなくてはならなくなる。これを自動化するために、今度は、「ファイルの置き場所に関する規約」を作ることにする。

def load_adapters
  lib_dir = File.dirname(__FILE__)
  full_pattern = File.join(lib_dir, 'adapter', '*.rb')
  Dir.glob(full_pattern).each { |file| require file }
end

上記のコードの通り、新しいアダプタの規約として「adapterディレクトリに置く」と規定しそれを守ることで、自動で新しいアダプタをrequireできるようになる。
ここまでのアイデアを盛り込んだMessageGatewayクラスの全体をみてみる。

class MessageGateway
  def initialize
    load_adapters
  end

  def process_message(message)
    adapter = adapter_for(message)
    adapter.send_message(message)
  end

  def adapter_for(message)
    protocol = message.to.scheme
    adapter_name = protocol.capitalize + 'Adapter'
    adapter_class = self.class.const_get(adapter_name)
    adapter_class.new
  end

  def load_adapters
    lib_dir = File.dirname(__FILE__)
    full_pattern = File.join(lib_dir, 'adapter', '*.rb')
    Dir.glob(full_pattern).each { |file| require file }
  end
end

アダプタの製作者に軽い制約を強いる代わりに、アダプタの追加を容易にしている。

クラスの自動生成

規約が決まったので、単語を渡すだけでクラスのジェネレーターを作ることができる。

protocl_name = ARGV[0] # コマンドライン引数を受け取る
class_name = protocol_name.capitalize + 'Adapter'
file_name = File.join('adapter', protocol_name + '.rb')

scaffolding = %Q{
class #{class_name}
  def send_message(message)
    # メッセージを送信するコード
  end
end
}

File.open(file_name, 'w') do |f|
  f.write(scaffolding)
end

このコードを例えばadapter_scaffolding.rbというファイルに保存し、以下のように実行することでFTP用のアダプタクラスの雛形を生成することができる。

ruby adapter_scaffolding.rb ftp

adapterディレクトリの中のftp.rbという名前のファイルにFtpAdapterという名前のクラスが生成される。

要点

  • Convention over ConfigurationのパターンはRubyの動的な特性を利用したパターン。
  • クラス名やメソッド名、ファイル名、標準的なディレクトリ構造などに基づく規約を使ってコードを生成することで、より扱いやすく拡張性に優れたシステムを構築できる。

おわりに

  • 使いどころ以外で使わないようにというアドバイスが本全体を通して強調されていた。そういう失敗多そう。
  • Rubyの魔術使うとデバッグはしづらくなりそうなので、コード見る他の人のことも考慮しないと。
  • デザインパターン勉強してから世にあるコードや本を見ると、いたるところでパターンやそれに類するものが使われているのが分かる。そういうコードに出くわした時に、「そのパターンも知ってる」と沢北のようにドヤれる。実際にGoの勉強中にhttp.Handler型の値を別のhttp.Handler型の値でラップするというテクニックが出てきた時、Decoratorパターン!!となった。コード読んでてパターン出てくると理解がいきなり促進されるので大変助かる。
  • まとめを公開するのとてもよい。今までは読んだ本のまとめはメモ帳などに書いていたが、公開前提のまとめ作りながら読むとかなり定着が促進しそう(ラーニングピラミッド)。実務ですぐに使わなそうなものは特に優先的にどこかに公開したい。
784
793
13

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
784
793

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?