[技術講座]
(株)オージス総研
福田 直樹
今回は、生成に関するデザインパターンを取り上げます。 通常、何らかのオブジェクト利用する際には、コンストラクタを呼び出してオブジェクトを生成し、そのオブジェクトを利用します。しかし、その際には利用する側では利用したい具象クラスを指定する必要があり、クラス間の結合度が強くなってしまいます。それよりも柔軟性のある生成の仕組みを提供するFactory Methodパターンを取り上げます。
※雑誌『Java WORLD』 2006 年 7 月号に掲載した記事のオリジナル原稿を Java WORLD 編集部の了解を得て掲載しています。
前回は、重複した実装を洗練、整理するデザインパターンとして Template Method パターンを取り上げました。受講情報をファイル出力する機能を取り上げ、FileSaver クラスでは処理のステップを実装し、そのサブクラスである CSVFileSaver クラス、HTMLFileSaver クラスでそれぞれの独自の実装を行いました。既存のソースコードでは重複した実装が存在し、処理の流れがわかりづらくなっていたものが、うまく役割分担され実装が整理されたことがおわかりいただけたと思います。Template Method パターンは、継承を使用するような場面では適用頻度も多いため、皆さんも比較的イメージしやすかったのではないでしょうか? A 君は、徐々にデザインパターンの理解度も上がってきているようです。今回もまた、A 君と一緒にデザイン・パターンについて学んでいきましょう。
今回は、生成に関するデザインパターンを取り上げます。 通常、何らかのオブジェクト利用する際には、コンストラクタを呼び出してオブジェクトを生成し、そのオブジェクトを利用します。しかし、その際には利用する側では利用したい具象クラスを指定する必要があり、クラス間の結合度が強くなってしまいます。それよりも柔軟性のある生成の仕組みを提供する Factory Method パターンを取り上げます。
前回 Template Method パターンを適用した後、企画部の方から TAB 区切り形式でもファイル保存をしてほしいという要望が挙がったようで、A 君は待ってましたとばかりに機能追加を行ったようです。ただ、機能の追加は簡単だったようですが、それに付随してその他の部分で修正が必要になってしまったようです。早速、A 君と C さんの話を聞いてみましょう。
A 君 「先週、企画部からお願いされていた TAB 区切り形式でのファイル保存の機能ですが、Template Method パターンを使っていたことで修正がかなり簡単でしたよ。FileSaver クラスを継承して TabFileSaver クラスを定義し、FileSaver クラスで定義されている抽象メソッドをオーバーライドすればよかったわけですからね。デザインパターンを使っていた甲斐がありました。ファイル保存機能を実現するクラス構造はこのようになっています。(図 1)」
図 1 :ファイル保存機能を実現するクラス群 |
C さん 「Template Method パターンの効果を早くも実感できたわけだね。デザインパターンは、変更容易性を高めてくれることが 1 つの大きなメリットだからね。」
A 君 「そうですね。ただ、TAB 区切り形式の保存機能を追加するのは簡単だったんですが、それを利用する側の修正は結構多かったんです。ParticipationList クラスもファイル保存機能を利用しているんですが、ソース・コードはリスト 1 のようになっています。」
リスト 1 : ParticipationList クラスで FileSaver を利用している部分のソース・コード |
// FileSaver を利用するクライアントクラス public class ParticipationList implements Aggregate { private List participations; public ParticipationList() { this.participations = new ArrayList(); } // .....略 ..... public void save(int saveType) throws Exception { // .....略 ..... FileSaver fileSaver = null; // 具象クラスを生成する if (saveType == FileTypeConstants.CSV_FILE_TYPE) { fileSaver = new CSVFileSaver(); } else if (saveType == FileTypeConstants.HTML_FILE_TYPE) { String encoding = System.getProperty("data.encoding"); fileSaver = new HTMLFileSaver(encoding); } else if (saveType == FileTypeConstants.TAB_FILE_TYPE) { fileSaver = new TabFileSaver(); } // 初期設定をする fileSaver.config(); // 保存機能を利用する fileSaver.save(this); // .....略 ..... } } // 定数宣言しているクラス public class FileTypeConstants { private FileTypeConstants() {} public static final int CSV_FILE_TYPE = 1; public static final int HTML_FILE_TYPE = 2; public static final int TAB_FILE_TYPE = 3; } |
C さん 「なるほど。ParticipationList のソース・コードを見ると、渡された saveType(保存するファイル形式) によって、生成する具象 FileSaver オブジェクトを作り分けているんだね。そして、その後 save メソッドの呼び出しをしているね。ファイル保存形式が増えたとしたら、このあたりの部分を修正しなければいけないわけだ。」
A 君 「そうなんです。せっかくファイル保存機能は機能追加をしやすくしたのに、ここを修正しないといけないのはどうにかしたいんですよね。」
C さん 「確かに if 文をいちいち修正しなければいけないのは面倒だね。あと、ここの部分は利用する具象クラスに直接依存しているから、ParticipationList クラスとそれぞれの具象クラスの結合度が強くなってしまっているのが気になるな。」
A 君 「そうなんですが。そのあたりのうまい洗練のやり方を C さんにお聞きしようと思って。」
C さん 「1 つのポイントとしては、現状オブジェクトを生成する際にはコンストラクタを呼び出しているけど、コンストラクタを呼び出さないようにする、ということが考えられるよ。正確に言うと『コンストラクタの呼び出しをファイル保存機能を利用しているクラス (クライアントクラス) の実装に含めない』ということかな。コンストラクタは特別なメソッドであって、それを呼び出すということは、オブジェクトの生成にあたって具象クラスを直接指定することになるからね。(図 2)そうなると、クラス間の結合度が強くなってしまってクラスの保守性や再利用性が低い構造になってしまうんだ。」
図 2 :クライアントクラス(ParticipationList)は具象クラスと直接依存している |
A 君 「でも、普通はオブジェクトにアクセスする際にコンストラクタを呼び出して、その後メソッド呼び出ししますよね。」
C さん 「そうなんだけどね。解決策はもう少し後にして、まずは問題点を整理しよう。もう 1 つ聞きたいんだけど、ファイル保存機能のクライアントクラスはその他にもあるのかな?」
A 君 「はい。こういうような実装はいくつかのクライアントクラスに存在してます。」
C さん 「なるほど。同じような実装部分というのはやはり 1 つにまとめた方がいいね。じゃ、ここまでの内容で現状の問題点を整理しておこうか。」
C さんは、現状の問題点をノートに整理しました。
C さん 「最初に修正したい点は、クライアントクラスの同じような実装部分を 1 つにまとめることかな。各クライアントクラスで同じ実装部分というのはどこになりそうかい?」
A 君 「えーっと、生成処理の if 文と、config メソッドの呼び出しもそうですね。これは生成した後に初期設定を行うメソッドなので。これらの部分は各クライアントクラスで同じ流れになってます。その後 save メソッド等の呼び出しをして FileSaver オブジェクトを利用しています。」
C さん 「その部分をクライアントクラスから外出しにして、別クラスとして抽出してみてくれるかな。」
A 君 「わかりました。やってみます。」
C さん 「あと、生成の機能を持つクラスは、一般的に『ファクトリ』と言うことが多いから、そういう名前を付けてくれるかな。ファクトリは、工場という意味だよね。工場 (ファクトリ) は何かを製造してくれる役割を持つということだね。」
A 君は、C さんの言われたことを頭に入れながら修正を行ったようです。ファイル保存機能のクライアントとしては、ParticipationList クラスのみを取り上げています。
A 君 「修正したソース・コードとクラス図は下のようになりました (リスト 2 、図 3) 。生成処理の if 文の部分と config メソッドの呼び出し部分を別の FileSaverFactory クラスの createSaver メソッドに定義しました。利用する側の PariticipationList クラスの save メソッドがすっきりしましたね。あと、工夫したこととして、createSaver メソッドはスタティックメソッドにしてみました。具象 FileSaver クラスを利用する際にいちいち FileSaverFactory クラスのインスタンスを作る必要はないかなと思って。」
リスト 2 :シンプルなファクトリクラスを導入したソース・コード |
// シンプルなファクトリクラス public class FileSaverFactory { // スタティックメソッドとして定義したファクトリメソッド public static FileSaver createSaver(int saveType) { FileSaver fileSaver = null; if (saveType == FileTypeConstants.CSV_FILE_TYPE) { fileSaver = new CSVFileSaver(); } else if (saveType == FileTypeConstants.HTML_FILE_TYPE) { String encoding = System.getProperty("data.encoding"); fileSaver = new HTMLFileSaver(encoding); } else if (saveType == FileTypeConstants.TAB_FILE_TYPE) { fileSaver = new TabFileSaver(); } fileSaver.config(); return fileSaver; } } // FileSaver を利用するクライアントクラス public class ParticipationList implements Aggregate { public void save(int saveType) throws Exception { // .....略 ..... // シンプルなファクトリクラスのスタティックメソッドを呼び出す FileSaver fileSaver = FileSaverFactory.createSaver(saveType); fileSaver.save(this); // .....略 ..... } } |
図 3 :シンプルなファクトリクラスの適用 FileSaverFactory クラスにスタティックメソッドが定義されている |
C さん 「ふむふむ、よく考えたね。生成するメソッド(ファクトリメソッド)をスタティックメソッドにするというのはよく使用されるテクニックなんだ。ファクトリクラスのオブジェクトは永続的に存在しなくてもいいことが多いからね。今回もファクトリクラスのオブジェクトで何らかの属性を持っているわけじゃないしね。それじゃ、これで現状の問題点は解決できたかな?」
A 君 「えーっと、生成処理の部分は共通化できて、クライアント側には FileSaverFactory に対しての createSaver メソッド呼び出しで FileSaver オブジェクトが返却されるようになってます。具象 FileSaver オブジェクト生成の責務は FileSaverFactory クラスに割り当てられていますね。それによって、クライアント側からコンストラクタの呼び出しもなくなったので、具象クラスへの依存もなくなっています。ただ、3 つ目の問題点は。。。」
C さん 「うん。これらの点は解決できているけど、この問題はまだ残ってることになる。」
- 生成処理の中に変動しやすい if 文が存在している
C さん 「保存形式のタイプによって処理を切り替えているから、具象 FileSaver クラスが追加された場合は、FileSaverFactory クラスの修正が必要になってしまうね。生成のロジック自体も柔軟性のある構造にするために、もう 1 段階修正を加えてみようか。」
C さん 「もう 1 段階の修正は、FileSaverFactory クラスの構造を変更しよう。ファイル保存形式によって、FileSaverFactory クラスのサブクラスを作成して、それぞれのサブクラスで具象 FileSaver オブジェクトの生成を行うようにするんだ。」
A 君 「なるほど。そうすると、何か具象 FileSaver クラスが追加された場合に FileSaverFactory クラスにも、それを利用するクラス側にも修正は必要なくて、具象 FileSaverFactory クラスを追加してあげればいいわけですね。あ、でも具象 FileSaverFactory オブジェクトの生成をしなければいけませんね。」
C さん 「うん。ParticipationList クラスでは具象 FileSaverFactory クラスに依存したくないから、PariticipationList クラスのコンストラクタで FileSaverFactory オブジェクトを受け取るような設計にしよう。そうすると、実行時に生成するクラスを指定してもらえば、それに従った具象 FileSaverFactory オブジェクトを受け取ってそれに応じた処理ができるようになる。」
A 君 「わかりました。機能追加に対応しやすい構造になりますね。じゃ、早速修正します!」
C さん 「まあまあ、そう慌てないで。このまま FileSaverFactory クラスのサブクラスを作成してもうまくいかないんだよ。なぜかわかるかい?」
A 君 「えっ、どういうことですか?」
C さん 「現状、FileSaverFactory クラスの createSaver メソッドはスタティックメソッドとして定義しているよね。スタティックメソッドは、各クラスに固定的に定義されたメソッドになるから、ポリモフィズムが使えないんだ。せっかくサブクラス化しても、利用するクラス側で具象クラスを意識しないといけないわけなんだよ。」
A 君 「そうですか。スタティックメソッドを変更しないといけないんですね。せっかく考えたんですけど、、、よくなかったんですね。
C さん 「いやいや、どこまで柔軟性を高めるかということだけど、サブクラス化するとさらにクラスが増えて構造は少し複雑になるから、メリットばかりというわけではないんだ。複雑さと柔軟性のトレードオフだね。今回は柔軟性の方をとったけど、どちらが正しいというわけではないんだよ。」
A 君が最終的に修正したソース・コードはリスト 3 のようになります。クラス図は図 4 となります。FileSaverFactory の createSaver メソッドを抽象メソッドとして定義し、CSVFileSaverFactory などのサブクラスの方ではそれをオーバーライドしてそれぞれの具象クラスを生成しています。FileSaverFactory クラスの継承階層では前回取り上げた Template Method パターンが使用されており、Factory Method パターンはその応用とも言うことができます。
リスト 3 : Factory Method パターンを適用したソース・コード |
// 抽象クラスとして定義した FileSaverFactory クラス public abstract class FileSaverFactory { // ファクトリメソッド public FileSaver create() { FileSaver fileSaver = createSaver(); fileSaver.config(); return fileSaver; } // 抽象メソッドとして定義する abstract protected FileSaver createSaver(); } // CSVFileSaver オブジェクトを生成するファクトリクラス public class CSVFileSaverFactory extends FileSaverFactory { protected FileSaver createSaver() { return new CSVFileSaver(); } } // HTMLFileSaver オブジェクトを生成するファクトリクラス public class HTMLFileSaverFactory extends FileSaverFactory { protected FileSaver createSaver() { String encoding = System.getProperty("data.encoding"); return new HTMLFileSaver(encoding); } } // TabFileSaver オブジェクトを生成するファクトリクラス public class TabFileSaverFactory extends FileSaverFactory { protected FileSaver createSaver() { return new TabFileSaver(); } } // FileSaver を利用するクライアントクラス public class ParticipationList implements Aggregate { private List participations; private FileSaverFactory fileSaverFactory; // コンストラクタで FileSaverFactory オブジェクトを取得する public ParticipationList(FileSaverFactory fileSaverFactory) { this.participations = new ArrayList(); this.fileSaverFactory = fileSaverFactory; } // .....略 ..... public void save() throws Exception { // .....略 ..... FileSaver fileSaver = fileSaverFactory.create(); fileSaver.save(this); // .....略 ..... } } |
図 4 : Factory Method パターンの適用
FileSaverFactory クラスのサブクラスを定義する。FileSaverFactory に 抽象メソッドとして定義された createSaver() をサブクラスでオーバーライド する。 |
それでは、今回紹介した Factory Method パターンの基本構造を確認しておきましょう。このデザインパターンは、生成したいオブジェクトのコンストラクタを呼び出してインスタンス化するのではなく、ファクトリ (生成する) メソッドを定義し、それを呼び出すことでインスタンス化を行うようにするものです。さらに、サブクラス化を行って、生成する具象クラスをサブクラスの方に任せる形にします。
その基本構造は、図 5 のようになります。Creator 側の継承階層が生成の責務を負い、今回の例で言うと FileSaverFactory クラス、具象 FileSaverFactory クラス群となります。Product 側の継承階層は実際にクライアントが利用したいクラスであり、FileSaver クラス、具象 FileSaver クラス群が該当します。(表 1)
図 5 : Factory Method パターンの基本クラス構造 |
表 1 : Factory Method パターンの構成要素 | ||||||||||||||||||
|
また、Factory Method パターンを適用することで、クラス間の結合度を疎に保ちつつ実行時に必要なオブジェクトを切り替えて利用することができることになります。これは、開発時に用意できなかったクラスを後から持ってきたり、今後開発する可能性のあるクラスに対して簡単な切り替えを提供するものになります。
基本構造はこの通りなのですが、この Factory Method パターンはシンプルなデザインパターンであり、活用方法にいろいろなバリエーションがあります。そのため、Factory Method パターンの適用例として目にするものには少し違った形式のものがあるでしょう。( 補足 A) 今回取り上げた例に関係するものを少し挙げると、以下のようなものがあります。
3 つめに挙げた例のようには、ファクトリクラス自体を抽出しないものも Factory Method パターンの一種と言うことができます。それは、Factory Method パターンの特徴はコンストラクタを呼び出すのではなく、ファクトリメソッドを呼び出してオブジェクトを生成するというものだからです。そのため、利用したいクラスに対するクライアントが 1 つであった場合には、そのクライアントのクラスの中にファクトリメソッドを定義し、サブクラスの方でファクトリメソッドをオーバーライドして生成するオブジェクトを切り替えるというような方法も考えられます。そうした場合には、クラス構造が単純になるというメリットはありますが、クライアント側のクラスは、生成以外の処理を行うのが通常ですし、生成させるクラスが増えるたびに利用側のクラスをサブクラス化する必要があることになります。どれを使用するかは、クライアントクラスの数や責務のバランスを確認して、使い分ける必要があります。
このように、デザインパターンはそれぞれの構成要素があり、クラス構造のモデルがありますが、適用する場面によって、変化させる必要があります。Factory Method パターンはその典型とも言えるものでしょう。そのため、デザインパターンのクラス構造をそのまま単に暗記するのではなく、解決したい問題や適用の目的やしっかり把握した上でクラス設計を行う必要があります。どちらかというと、設計時には「デザインパターンを適用しよう」という意識よりも、オブジェクト指向設計におけるクラスの責務分割の一環としてクラス設計し、デザインパターンは参考程度にとどめるという視点で捉えることも有効です。
補足 A 様々なバリエーションについてさらに詳しく学習したい方は、「パターン指向リファクタリング入門」 (著者: Joshua Kerievsky) の「6 章:生成」が参考になります。デザインパターンの中でも Factory Method と Abstract Factory のように似たようなデザインパターンが存在しますが、違いやそれぞれの設計指針のバリエーションを整理することができます。
© 2007 OGIS-RI Co., Ltd. |
|