はじめに
デザインパターンって常識っぽいからちゃんと学んでおかないとと思いつつ、いまいちよく分からないままな人って意外と多いんじゃないかと思い、絶版になってることもありまとめてみることに。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 '/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パターン!!となった。コード読んでてパターン出てくると理解がいきなり促進されるので大変助かる。
- まとめを公開するのとてもよい。今までは読んだ本のまとめはメモ帳などに書いていたが、公開前提のまとめ作りながら読むとかなり定着が促進しそう(ラーニングピラミッド)。実務ですぐに使わなそうなものは特に優先的にどこかに公開したい。