Ruby によるデザインパターン
この本は絶版になっており非常に高い。
中古を高いお金を出して買わずに https://github.com/davidgf/design-patterns-in-ruby や日本語でのまとめを探して読むので十分だと思う。
このまとめはコードは載せていないが、コードを載せているまとめも多くあるので多くあるので探してみるとよい。
1. よいプログラムとパターン
GoF 本に対する筆者の要約は次の 5 つのポイントになる。
- 変わるものを変わらないものから分離する
- インターフェイスに対してプログラムし、実装に対して行わない
- 継承より集約
- 委譲、委譲、さらに委譲
- 必要になるまで作るな(YAGNI = You Ain't Gonna Need It)
Ruby によるデザインパターンでも同様にポイントになっているので常に意識して読むと良さそう
2. Ruby をはじめよう
Ruby の文法について
3. アルゴリズムを変更する: Template Method
- 概略
- 骨格となるメソッド(テンプレートメソッド)を持った抽象基底クラスを構築し、サブクラスが実際のテンプレートメソッドの実際の処理(フックメソッド)を提供する。
- テンプレートメソッドは NotImplementedError を raise するメソッドかもしれないし何かしらの標準実装かもしれない。
- 大枠の処理は抽象基底クラスに定義してあるので、サブクラスは断片的に見えるかもしれない。
- 骨格となるメソッド(テンプレートメソッド)を持った抽象基底クラスを構築し、サブクラスが実際のテンプレートメソッドの実際の処理(フックメソッド)を提供する。
- メリット
- 継承を使ってアルゴリズムに多様性をもたせることができる
- 抽象基底クラスはテンプレートメソッドを通じて高レベルの処理の制御に集中できる。サブクラスはその詳細を埋める
- デメリット・注意点
- 継承を使っていることは問題になりうる。継承より集約
- 抽象基底クラスとサブクラス間で将来的にも is-a という関係を保ち続けられるか?(リスコフの原則)
- 継承階層が深くなってしまい可読性が落ちてしまわないか?
- YAGNI
- 最初からテンプレートメソッドで多くの場合に対処できるようにはしない。最初はシンプルな実装で必要になってからこのパターンを適用する。
- 大量のフックメソッドの実装を強いるテンプレートクラスを作らない。
- 継承を使っていることは問題になりうる。継承より集約
4. アルゴリズムを交換する: Strategy
- 概略
- Template Method では継承を使って
変わるものを変わらないものから分離する
を実現したが Strategy では委譲
によってこれを実現する- 変わる部分を別のオブジェクトに切り出し、利用したいオブジェクトは切り出したオブジェクトを取替可能なパーツとして扱う。
- 切り出された同じ目的を持ったオブジェクト群を
Strategy
と呼び、Strategy の利用者をContext
と呼ぶ。 - コンテキストからストラテジに渡す引数はコンテキスト自身にするケースもある(もともとはストラテジはコンテキストの一部なので合理的に思える)
- 簡易的なストラテジであれば Proc オブジェクトを使って実装することもできる
- 切り出された同じ目的を持ったオブジェクト群を
- 変わる部分を別のオブジェクトに切り出し、利用したいオブジェクトは切り出したオブジェクトを取替可能なパーツとして扱う。
- Template Method では継承を使って
- メリット
- 委譲を使ってアルゴリズムに多様性をもたせることができる
- ストラテジを切り出すことで関心を分離できる
- 移譲と集約に基づいている(継承ではない)ので、実行時にストラテジの切り替えが簡単にできる
- デメリット・注意点
- コンテキストとストラテジのインターフェイスに一貫性をもたらさないと、ストラテジはあくまでコンテキストから切り出したものである、という関係性が崩れる
- コンテキストと特定のストラテジの依存関係を高めると複数のストラテジへの対応が難しくなるのでメリットが小さくなる
5. 変更に追従する: Observer
- 概略
- 変更が発生したオブジェクト(Subject, Observable)と変更の発生に関心のあるオブジェクト(Observer)間で、上手く変更を伝搬する
- Observer パターン用の Ruby 標準ライブラリ: https://github.com/ruby/ruby/blob/master/lib/observer.rb
- オブザーバーは変更の発生時に呼び出すための共通のインターフェイスを持っており、Subject はイベント発生時に一連のオブザーバーのインターフェイスを呼び出す。
- 変更が発生したオブジェクト(Subject, Observable)と変更の発生に関心のあるオブジェクト(Observer)間で、上手く変更を伝搬する
- メリット
- デメリット・注意点
- 特にイベント通知が多い場合、不必要な通知を送ることで全体のパフォーマンスが落ちる可能性がある。
- その他
6. 部分から全体を組み立てる: Composite
- 概略
- メリット
- 任意の深さの木構造を構築でき、どのノードも同じように扱うことができる
- デメリット・注意点
- Leaf と Composite を実装上どう扱うかに注意
- ツリーの深さが1段しかないと仮定してしまうとおかしくなってしまう
7. コレクションを操作する: Iterator
- 概略
- メリット
- 実装の詳細を知ることなく、集約オブジェクトの子オブジェクトを走査し任意の走査を行うことができる
- #each と #<=> を実装すれば Enumerable モジュールを include することで便利メソッドがたくさん手に入る
- デメリット・注意点
- コレクションの走査中にコレクションに変更を加えると網羅的に走査できないかも
8. 命令を実行する: Command
- 概略
- オブジェクトからある特定の動作を切り出したオブジェクトにする(Strategy に近い?)
- Composite パターンと合わせて、1 つのタスク実行で一連の Command を実行するパターンも形成できる
- Obsever パターンと多くの共通点を持つ
- Command: 動作の方法を知っているだけで呼び出し元の状態には関心がない
- Observer: 呼び出される対象の状態に応じて動作する
- メリット
- デメリット・注意点
9. ギャップを埋める: Adapter
- 概略
- メリット
- 新たにインターフェイスを満たすクラスを 1 から作ることなく既存のクラスを再利用できる
- デメリット・注意点
- 実装方針の選定基準
- オブジェクトの変更: 対象をよく理解していて、インターフェイスの変更が比較的少ない場合
- アダプタクラスの実装: オブジェクトが複雑な場合やオブジェクトへの理解が不十分な場合
- 実装方針の選定基準
10. オブジェクトに代理を立てる: Proxy
- 概略
- クライアントと対象オブジェクトの間に対象オブジェクトと同じインターフェイスを持つオブジェクトを挟みいくつかの機能を提供する
- Protection Proxy: 対象オブジェクトへのアクセス制御を行う
- Remote Proxy: 実際にはネットワークを介した別サーバに存在する対象オブジェクトが実際に存在しているかのように振る舞う(RPC)
- Virtual Proxy: 対象オブジェクトの生成を必要になるまで遅らせる(||= 演算子を使う)
- https://github.com/wantedly/computed_model などは Proxy の例
- クライアントと対象オブジェクトの間に対象オブジェクトと同じインターフェイスを持つオブジェクトを挟みいくつかの機能を提供する
- メリット
- Protection Proxy: 対象オブジェクトからアクセス制御の責務を分離できる
- Remote Proxy: クライアントは対象オブジェクトがどこにあるかを意識しなくて良くなる
- Virtual Proxy: オブジェクト生成コストを遅らせる、削減できる
- デメリット・注意点
11. オブジェクトを改良する: Decorator
- 概略
- 基本となる機能をを実装する ConcreteComponent を副次的な機能を持つ Decorator でラップしチェーンして呼び出す
- rack middleware は Decorator の例。middleware は #call のインターフェイスを持ち、次々に次の rack app を呼び出していく
- メリット
- 必要な機能を実行時に組み立てることができる
- 基本機能と Decorator、Decorator 毎の関心を分離できる
- デメリット・注意点
- 必要な機能を実行毎に組み立てるのが煩雑になりうる
- デコレータの連鎖によるパフォーマンスの低下。一つの大きなオブジェクトでいいかもしれない
Adapter と Proxy と Decorator
- これら 3 パターンは別のオブジェクトの代理オブジェクトの役割を果たしている
12. 唯一を保証する: Singleton
- 概略
- インスタンスが 1 つしかなく、そのオブジェクトをグローバルにす
- singleton モジュールを include する方法、モジュールで実装する、クラスメソッドを使って実装するなどいくつかの方法がある
- メリット
- 設定ファイルやロガーなど、1 つだけ存在すればいいオブジェクトを引き回さなくて良くなる
- デメリット・注意点
- 本当にグルーバル変数にする必要があるのか?
- 1 つだけ存在しているというのは確かなのか?
13. 正しいクラスを選び出す: Factory
- 概略
- クラス選択をサブクラスに任せる
- 親クラスが振る舞いを持ち、サブクラスがどのクラスを選択するかを持つ
- Abstract Factory は、有効なクラス選択の組み合わせを決めておく
- クラス選択をサブクラスに任せる
- メリット
- 共通の振る舞いを持つ別のクラスを簡単に作成できる
- 振る舞いとクラス選択の責務を分離できる
- デメリット・注意点
14. オブジェクトを組み立てやすくする: Builer
- 概略
- メリット
- 複雑なオブジェクトの生成・生成時のパラメータのバリデーションを別のクラスに切り出すことができる
- デメリット・注意点
- YAGNI: 単純なインスタンス化でいいところに Builer を適用して不必要にコードを複雑にすること
- Builder クラスを用意するより
self.build
というメソッドを定義し、そのメソッドにインスタンス化を任せるケースも多い- Builder クラスを用意する例
- https://github.com/rack/rack/blob/f9ad97fd69a6b3616d0a99e6bedcfb9de2f81f6c/lib/rack/builder.rb
- config.ru から rack app を組み立てる
- 別のクラスとして返すので Factory でもある?
- https://github.com/rack/rack/blob/f9ad97fd69a6b3616d0a99e6bedcfb9de2f81f6c/lib/rack/builder.rb
- self.build でインスタンス化をする例
- Builder クラスを用意する例
15. 専用の言語で組み立てる: Interpreter
- 概略
- 課題を解決するための専用の言語を作る
- 専用の言語をパースして個別のノードが実行可能な抽象構文木(AST)にする
- 課題を解決するための専用の言語を作る
- メリット
- 専用の言語によってシンプルな記述によって課題を解決することができる
- デメリット・注意点
- パーサーを作ることが複雑になりうる
- Interpreter パターンを利用することで課題をシンプルに解決できるにも関わらず適用しないことで複雑なまま課題に向かうことになる
Ruby のためのパターン
- DSL(Domain Specific Language)
- class_eval・instance_eval を活用して DSL を作ろう。
- rack DSLやrails の routesなどが有名?
- class_eval・instance_eval を活用して DSL を作ろう。
- メタプログラミング
- 動的にオブジェクトやメソッドを作る。
- メタプログラミングRubyは本当にいい本。
- Convention over Configuration
まとめ
冒頭の筆者の GoF 本に対するまとめの再掲
- 変わるものを変わらないものから分離する
- インターフェイスに対してプログラムし、実装に対して行わない
- 継承より集約
- 委譲、委譲、さらに委譲
- 必要になるまで作るな(YAGNI = You Ain't Gonna Need It)
この辺りはオブジェクト指向実践設計ガイドでも大体同じことを言っていた。
原則としては TRUE
・SOLID
などがこのポイントに当てはまってくるのではないだろうか。
- TRUE
- Transparent
- Reasonable
- Usable
- Exemplary
- SOLID
- Single Responsibility Principal
- Open Closed Principal
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle