第1章 よいプログラムとパターン
パターンのためのパターン
Gofのデザインパターンのアイデアを要約すると、次の4つのポイントになる。
1. 変わるものより変わらないものから分離する
ソフトウェア開発における全ての変更は局所的なので、全てのコードをくまなく調べる必要がないようにすべき。
2. インターフェイスに対してプログラムし、実装に対して行わない
ここでのインターフェイスとは、JavaやC#における抽象インターフェイスではなく、可能な限り一般的な型のことを指す。
例) 飛行機、電車、自動車 に対する 「乗り物」
インターフェイスに対するプログラミングの結果、
結合度が下がり、少しのクラスの変更だけで済む、変更に対して強いコードになる。
3. 継承より集約
- 継承(is-a-kind-of)
- サブクラスとスーパークラスの結合度が高いという問題がある。
- 集約(has-a)
- 各クラスの再利用性が高められる。
- カプセル化ができる。
4. 委譲、委譲、委譲
集約されたオブジェクトに責任転嫁させるメソッドを書く。
継承よりも柔軟で副作用がなくなる。
サンプルコード
継承を使ったコードの例
class Vehicle
# ...
def start_engine
# エンジンをスタート
end
def stop_engine
# エンジンをストップ
end
end
class Car < Vehicle
def sunday_drive
start_engine
# 地方に出かけ、戻ってきます。
stop_engine
end
end
集約と委譲を使ったコードの例
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
def start_engine
@engine.start
end
def stop_engine
@engine.stop
end
end
必要になるまで作るな
YAGNI = You Ain't Gonna Need It
- 将来必要とされるものを前もって作った場合、使われなかった場合はすべて無駄になり、増やしてしまった複雑さを抱え続けなければならなくなる。
- 本当に必要になるまで待てれば、何が必要で、どのようにすべきかのよりよい理解を持ちやすくなる。
本書で扱うGofの14パターン
- Template Method
- Strategy オブジェクト
- Observer パターン
- Composite パターン
- Iterator パターン
- Command パターン
- Adapter パターン
- Proxy
- Decorator パターン
- Singleton
- Factory Method
- Abstract Factory
- Builder パターン
- Interpreter
Rubyの中のパターン
- 内部ドメイン特化言語
- メタプログラミング
- Convention over Configuration(CoC)
第3章 アルゴリズムを変更する:Template Method
Template Methodパターンの一般的な考え方
- 変わらないもの(抽象基底クラスのテンプレートメソッド)と変わるもの(サブクラスのメソッド)を分離する
- 抽象メソッド(テンプレートメソッド)を呼び出そうとしたときは例外を投げる
- 誰もオーバーライドしないようなテンプレートメソッドを作ることは避ける
フックメソッド
- 抽象基底クラスで提供する、サブクラスの標準実装。
- 中身が空になる場合もある。
ダックタイピング
Rubyは、渡されてくるオブジェクトが特定のクラスに属していることを言語でチェックしない。
サンプルコード
# NOTE: 長くなるので1行メソッド使ってる
# 抽象クラス
class Report
def initialize
@title = '月次報告'
@text = ['順調', '最高の調子']
end
def output_report
output_start
output_head
output_body_start
output_body
output_body_end
output_end
end
def output_body
@text.each do |line|
output_line(line)
end
end
def output_start; end
def output_head; output_line(@title) end
def output_body_start; end
def output_line(line)
raise 'Called abstract method: output_line'
end
def output_body_end; end
def output_end; end
end
# 具象クラス1
class HTMLReport < Report
def output_start; puts('<html>') end
def output_head
puts(' <head>')
puts(" <title>#{@title}</title>")
puts('</head>')
end
def output_body_start; puts('body') end
def output_line(line); puts(" <p>#{line}</p>") end
def output_body_end; puts('</body>') end
def output_end; puts('</html>') end
end
# 具象クラス2
class PlainTextReport < Report
def output_head
puts("**** #{@title} ****")
puts
end
def output_line(line); puts(line) end
end
第4章 アルゴリズムを交換する:Stragtegy
Template Methodパターンのような継承ベースのテクニックは、スーパークラスへの依存をもたらすため、柔軟性に制限がある。
委譲、委譲、さらに委譲
- Strategyパターン
- 「別々のオブジェクトにアルゴリズムを引き出す」テクニック。
- Strategy = 同じインターフェイスを持った一群のオブジェクト。
- Strategyの利用者をContextと呼ぶ。
- Contextは、ストラテジオブジェクトを取り替え可能なパーツとして扱うことができる。
- 責務と知識、関心の分離ができる。
Procとブロック
- ブロック = クロージャー = ラムダ
- オブジェクトが作成された時の周囲の環境をProcオブジェクトが取り込んでくれる。
- 別の記事でまとめてる → 【Ruby】ブロック
コードブロックベースのストラテジ
サンプルコードの例は、下記のように変更することで単純化できる。
- Report クラスの initialize メソッドがコードブロックを受け取る。
- Report#output_report がコールするメソッドを output_report から call に変更する。
- *Formatter クラスのインスタンスを作る代わりに、Procオブジェクトを作る。
コードブロックベースのストラテジは、下記のような時に有効。
- インターフェイスが単純で、1つのメソッドで事足りる(callだけでしか呼び出されない)ような、シンプルなストラテジが要件に合うとき。
サンプルコード
# Context
class Report
attr_reader :title, :text
attr_accessor :formatter
def initialize(formatter)
@title = '月次報告'
@text = ['順調', '最高の調子']
@formatter = formatter
end
def output_report
@formatter.output_report(@title, @text)
end
end
# Strategy1
class HTMLFormatter < Formatter
def output_report(title, text)
puts('<html>')
puts(' <head>')
puts(" <title>#{title}</title>")
puts(' </head>')
puts(' <body>')
text.each do |line|
puts(" <p>#{line}</p>")
end
puts(' </body>')
puts('</html>')
end
end
# Strategy2
class PlainTextFormatter < Formatter
def output_report(title, text)
puts("***** #{title} *****")
text.each do |line|
puts(line)
end
end
end
# # How to use Strategy
# report = Report.new(HTMLFormatter.new)
# report.output_report
#
# report.formatter = PlainTextFormatter.new
# report.output_report
第5章 変更に追従する:Obsever
-
Observerパターン
- 「何らかのオブジェクトが変化した」というニュースの発信者と消費者の間にきれいなインターフェイスを作るアイディア。
- オブジェクトが Subject の状態の通知を受け取ることに関心があるとき、そのオブジェクトを Observer として Subject に登録する。
-
Subject(Observable オブジェクト)
- ニュースを持っているクラスのこと。
- Observerパターンでは Subject が通知作業のほとんどを行っており、Observer を絶えず把握する責任があるのは Subject。
-
Observer
- ニュースを得ることに関心のあるオブジェクト。
サンプルコード
# Subject
module Subject
def initialize
@observeres = []
end
def add_observer(observer)
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
def notify_observers
@obsevers.each do |observer|
observer.update(self)
end
end
end
class Employee
include Subject
attr_accessor :name, :title, :salary
def initialize(name, title, salary)
super()
@name = name
@title = title
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
# Observer
class Payroll
def update(changed_employee)
puts("#{changed_employee.name}のために小切手を切ります!")
puts("彼の給料はいま#{changed_employee.salary}です!")
end
end
# # How to use Observer
# fred = Employee.new('Fred', 'Crane Operator', 30000.0)
# payroll = Payroll.new
# fred.add_observer(payroll)
# fred.salary = 35000.0
# => Fredのために小切手を切ります!
# 彼の給料はいま35000.0です!
第6章 部分から全体を組み立てる:Composite
-
Composite パターン
- 「全体が部分のように振る舞う」という状況を表すデザインパターン。
- 複雑なオブジェクトが、個々のコンポーネントの特徴を共有している、つまり全体がその部分とよく似ている場合、Composite パターンがよく合う。
- 階層構造やツリー構造のオブジェクトを作りたいときに利用する。
- ツリーを利用するコードが1つの単純なオブジェクトを扱っているのか、それともごちゃごちゃした枝全体を扱っているのかを考えさせたくないとき。
- 「全体が部分のように振る舞う」という状況を表すデザインパターン。
-
Component クラス
- すべてのオブジェクトの共通のインターフェイスまたは基底クラス。
- 親子関係を管理する場所。
-
Leaf クラス
- プロセスの単純な構成要素で、1つ以上必要。インターフェイスを実装する。
- 子のオブジェクトを扱えないようにした方が良い。
-
Composite クラス
- Component としての役割を持ち、コンポーネントのコレクションとしての役割も持っている。
- サブコンポーネントから作られる、より上位のオブジェクト。
- 子のオブジェクトを追加または削除するメソッドが必要になる。
- Component としての役割を持ち、コンポーネントのコレクションとしての役割も持っている。
サンプルコード
# Component クラス
class Task
attr_reader :name, :parent
def initialize(name)
@name = name
@parent = nil
end
def get_time_required
0.0
end
end
# Leaf クラス1
class AddIngredientTask < Task
def initialize
super('Add dry ingredients')
end
def get_time_required
1.0 # 小麦粉と砂糖を加えるのに1分
end
end
# Leaf クラス2
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
task.parent = self
end
def remove_sub_task(task)
@sub_tasks.delete(task)
task.parent = nil
end
def get_time_required
time = 0.0
@sub_tasks.each { |task| time += task.get_time_required }
time
end
end
class MakeBatterTask < CompositeTask
def initialize
super('Make batter')
add_sub_task(AddDryIngredientTask.new)
add_sub_task(AddLiquidTask.new)
add_sub_task(MixTask.new)
end
第7章 コレクションを操作する:Iterator
-
Iterator パターン
- 集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する。
-
外部イテレータ
- イテレータ が 集約(下記の例では array 仮引数に代入されるオブジェクト)とは別のオブジェクトになっている。
- クライアントが繰り返しを駆動し、次の要素の準備ができるまで次を呼び出さない。
- 外部であるため共有が可能で、他のメソッドやオブジェクトに渡すことができる。
- イテレータオブジェクトが必要になる。
- 例: IO クラスのファイル操作
- イテレータ が 集約(下記の例では array 仮引数に代入されるオブジェクト)とは別のオブジェクトになっている。
-
内部イテレータ
- イテレータがコードブロックを使用することで、ロジックを集約に伝える。
- 集約オブジェクトは、それぞれの子オブジェクトに対してコードブロックを呼ぶことができる。
- シンプルであり、コードがわかりやすい。
- 別個のイテレータオブジェクトは必要ない。
- 例:Array, String, Enumerable, Hash クラス
- イテレータがコードブロックを使用することで、ロジックを集約に伝える。
サンプルコード
# 外部イテレータ
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 クラスは each というイテレータメソッドを持っているためそちらを使用する。
def for_each_element(array)
i = 0
while i < array.length
yield(array[i])
i += 1
end
end
# # How to use 内部イテレータ
# a = [10, 20, 30]
# for_each_element(a) { |element| puts("The element is #{element}") }
第8章 命令を実行する:Command
- Command パターン
- 動作用のコードをオブジェクトに抜き出し、それらをまとめた小さなパッケージを作る。
- 何を行うかの決定と、それの実行とを分離する。
- これから行うことのリストや、完了したことのリストを記録する必要がある場合に役に立つ。
- プログラムが行ったことを元に戻すこともできる。
- ActiveRecord のマイグレーション機能は、元に戻すことができる Command パターンの実装の典型例。
- 動作用のコードをオブジェクトに抜き出し、それらをまとめた小さなパッケージを作る。
サンプルコード
class Command
attr_accessor :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
def unexecute
File.delete(@path)
end
end
class DeleteFile < Command
def initialize(path)
super("Delete file: #{path}")
@path = path
end
def execute
File.delete(@path)
end
def unexecute
if @contents
f = File.open(@path, "w")
f.write(@contents)
f.close
end
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
# Composite
class CompositeCommand < Command
def initialize
@commands = []
end
def add_command(cmd)
@command << cmd
end
def execute
@commands.each { |cmd| cmd.execute }
end
def unexecute
@commands.reverse.each { |cmd| cmd.unexecute }
end
def description
description = ''
@commands.each { |cmd| description += cmd.description + "\n" }
description
end
end
# # How to use Command
# 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(''file1.txt'))
# cmds.execute
第9章 ギャップを埋める:Adapter
- Adapter
- 既存のインターフェイスと必要なインターフェイスとの間の深い溝を橋渡しするオブジェクト。
# Encrypter は クライアントのオブジェクトにあたる。
# クライアントが実際に持っているのは Adapter である StringIOAdapter への参照。
# StringIOAdapter クラスは外部からは普通のIOオブジェクトに見える。
# しかし、 StringIOAdapter クラスは adaptee である string から文字を取得する。
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
class StringIOAdapter
def initialize(string)
@string = string
@position = 0
end
def getc
if @position >= @string.length
raise EOFError
end
ch = @string[@position]
@position += 1
return ch
end
def eof?
return @position >= @string.length
end
end
# encrypter = Encrypter.new('XYZZY')
# reader = StringIOAdapter.new('We attack at dawn')
# writer = File.open('out.txt', 'w')
# encrypter.encrypt(reader, writer)
めんどくさくなったので完
-
オブジェクトに代理を立てる:Proxy
-
オブジェクトを改良する:Decorator
-
唯一を保証する:Singleton
-
正しいクラスを選び出す:Factory
-
オブジェクトを組み立てやすくする:Builder
-
専用の言語で組み立てる:Interpreter
-
オリジナル言語を作る:Domain-Specific Languages
-
カスタムオブジェクトを作る:メタプログラミング
-
Convention over Configuration
と続く。