具体的には、前回題材としたジョブコントローラを再度用いて、Template MethodパターンとFactory Methodパターンの組み合わせをAbstract Methodパターンに変換し、さらにラムダ式を活用するように書き換えます。
Template Methodパターンのおさらい
まずはTemplate Methodパターンについておさらいしましょう。Template Methodパターンは、スーパークラスに共通処理を集約し、さまざまに差し替えられる処理の細かい部分をサブクラスに任せるパターンです。典型的なTemplate Methodパターンのプログラムには、図1に示すようなクラスが登場します。
それぞれのクラスの役割は次のとおりです。
- AbstractClass:共通処理を表すテンプレートメソッドを実装するクラス。差し替えられる処理を抽象メソッドとして宣言する。テンプレートメソッドは差し替え可能な処理を呼び出す
- ConcreteClass:AbstractClassに宣言された差し替え可能な処理を実装するクラス
Template Methodパターンを使っている例としては、Java標準のコレクションフレームワークが挙げられます。たとえば、AbstractList
クラスと ArrayList
クラスは図2のような継承関係を持ちます。
ここでは AbstractList
クラスがAbstractClassに、ArrayList
クラスがConcreteClassに該当します。AbstractList
クラス(およびそのスーパークラスである AbstractCollection
クラス)では、isEmpty
、indexOf
、equals
などのメソッドが共通的に実装されており、iterator
、get
、size
の3メソッドが抽象メソッドとして宣言されています。したがって、ArrayList
のように具体的なリストの実装クラスは、最低限 iterator
、get
、size
の3メソッドだけを実装すれば、リストの機能を提供できます1。
Template Methodパターンは、クラス同士の継承関係が現れる多くのプログラムで用いられています。筆者の経験では、Template Methodパターンを用いていないクラス同士の継承関係は、大抵の場合、まずい設計です。このようなプログラムは、本稿の後半で解説する「継承から委譲へ」の方針を適用することで、設計を改善できます。
[1] 実際には、ArrayList
クラスは性能最適化のために isEmpty
メソッドや indexOf
メソッドなどをオーバーライドしています。
Factory Methodパターンのおさらい
Factory MethodパターンはTemplate Methodパターンの特殊な例といえます。具体的には、Template Methodパターンで差し替えられる処理が、オブジェクトを生成する処理である場合に、Factory Methodパターンが用いられていると考えられます。
典型的なFactory Methodパターンのプログラムには、図3に示すようなクラス、インタフェースが登場します。
それぞれのクラス、インタフェースの役割は次のとおりです。
- Creator: ファクトリメソッドを差し替え可能な処理として宣言する抽象クラス。Template MethodパターンのAbstractClassに相当する
- ConcreteCreator: ファクトリメソッドを実装するクラス。Template MethodパターンのConcreteClassに相当する
- Product: 生成されるオブジェクトのインタフェース
- ConcreteProduct: 実際に生成されるオブジェクトのクラス
ファクトリメソッドを差し替え可能とする動機は、Creatorに相当するクラスで新しいConcreteProductを生成・利用したくなったときに、Creatorクラス自体に手を入れずに済むようにすることです。
また、Factory Methodパターンを用いることで、テストが容易になる場合があります。たとえば、リスト1のWarblerクラスは、Twitter4jライブラリを利用してTwitterにさえずり声を投稿するウグイスのシミュレータです。Warbler
クラスを単体テストする際、いちいちTwitterにさえずり声が投稿されるのは望ましくないでしょう。リスト1では、Twitterへの投稿を行う Twitter
クラスのインスタンスを、ファクトリメソッドである makeTwitter
メソッドに切り出していますから、単体テストの際は、実際には投稿を行わないスタブやモックを生成するように、makeTwitter
メソッドをオーバーライドすればよいのです。
import twitter4j.*; /** ウグイス. */ public class Warbler1 { /** さえずる. */ public void warble() { Twitter twitter = makeTwitter(); try { twitter.updateStatus("ケキョケキョケキョ"); } catch (TwitterException exception) { throw new WarblerException("さへずらざりけり", exception); } } /** Twitterクライアントのファクトリメソッド. */ protected Twitter makeTwitter() { TwitterFactory twitterFactory = new TwitterFactory(); return twitterFactory.getInstance(); } }
先にも述べたように、前回題材としたジョブコントローラプログラムはFactory Methodパターンを採用しています。図4のクラス図を見てください。
このジョブコントローラプログラムでは、Factory Methodパターンの各役割はそれぞれ次のように担われています。
- JobDefinitionクラス: ジョブを生成して利用するCreator
- BackupJobDefinitionクラス: バックアップジョブを生成するConcreteCreator
- Jobクラス: Product
- BackupJobクラス: ConcreteProduct
これは特にひどい設計というわけではありません。しかし、ジョブのクラスを作るごとに、いちいち対応するジョブ定義のクラスを作る必要があるというところに、若干の野暮ったさが感じられます2。BackupJobDefinition
クラスは makeJob
メソッドを呼び出しているだけなのですから、なんとかラムダ式に置き換えられないか、と考えるところですが、残念ながら直接的にラムダ式を使うことはできません。JobDefinition
クラスは抽象クラスであって、関数型インタフェースではないからです。
ラムダ式を使ってよりよい設計を実現するためには、まずFactory MethodパターンのプログラムをAbstract Factoryパターンに変更する必要があります。
[2] Martin Fowler et al. 1999 "Refactoring: Improving the Design of Existing Code" では、この種の野暮ったさに“Parallel Inheritance Hierarchies”と名前を付け、「コードの臭い」として取り上げています。