はじめに
適当にネットサーフィンしてましたところ、プログラミングのデザインパターンの記事に行きつきました。
デザインパターンってあれですよね?
「なになに?コーディングしてて問題が出たって?大丈夫だ心配しないで。君の抱えている問題にぴーったりの解決方法があるよー。先人のとっても偉い人が考えた方法だから安心して使ったらいいよー」 みたいなやつですよね?
まあプログラミングする人ならそりゃ当然知っとるわー的なモノでしょうが、正直なところすべてを理解しているかと言われたら自分はノーでございます。全部で20個以上くらいありますよね?
今までの成果物に対してもデザインパターンのどれかを使っていることが多いんでしょうが、そこまで意識していないといいいますか、結果的にあのパターン使ってたよねーくらいな感じなのです。
プログラムを始める前の設計段階で、「うむ。これはあのパターンでいけそうだね!」な~んて言えたらカッコいいだろうな~とか思いました。
なもんで、時間もあったのでちゃんと理解してみるかーという軽い気持ちで、まずは「Factory Methodパターン」について調べてみました。
なんで「Factory Methodパターン」を最初に選んだかって?記事の一番最初にあったのが「Factory Methodパターン」だったからです。
Factory Methodパターン とは何ぞ?
まずは、「Factory Methodパターン」ってなんやねん?ってところからです。
リファクタリングとデザインパターンによると、
Factory Method (ファクトリー・メソッド) は、 生成に関するデザインパターンの一つで、 スーパークラスでオブジェクトを作成するためのインターフェースが決まっています。 しかし、 サブクラスでは作成されるオブジェクトの型を変更することができます。
だってさ。うん。わからん。わからんのは自分の理解力不足です。すみません。ちゃあんと知識が蓄積されていて理解力がある方ならわかるんだと思います。
具体的なコードがあればイメージできるんですがねえ。。。ということで、よくある「Factory Methodパターン」のコード例を見てみます。
コード例
複数の種類のドキュメントを扱うアプリケーションを例としてみていきましょう。
Product クラス
まずはドキュメントを表すクラスがこんな感じであります。ここでは、ドキュメントとしてWordやPDFを定義してみます。
これらは、Factory Methodパターン界隈では Product(製品) と呼ばれたりします。
public interface IDocument {
void Open();
void Close();
}
public class PDFDocument : IDocument {
public void Open() {
Console.WriteLine("PDFを開きました");
}
public void Close() {
Console.WriteLine("PDFを閉じました");
}
}
public class WordDocument : IDocument {
public void Open() {
Console.WriteLine("Wordを開きました");
}
public void Close() {
Console.WriteLine("Wordを閉じました");
}
}
各ドキュメントクラスは IDocument
インターフェースを実装しているので、各クラス Open
、Close
2つのメソッドの実装が必要となるわけですね。まーここについては特に疑問はありません。
ファクトリークラス
次は上で定義した各ドキュメントクラスのインスタンスを作成する役割を担うクラスを定義します。
Factory Method 界隈ではこれらは ファクトリークラス と呼ばれたりします。Product(製品)のインスタンスを作成するんだからファクトリーってことっすね。多分。
public abstract class DocumentCreator {
// Factory Method
public abstract IDocument CreateDocument();
}
public class PDFCreator : DocumentCreator {
public override IDocument CreateDocument() {
return new PDFDocument();
}
}
public class WordCreator : DocumentCreator {
public override IDocument CreateDocument() {
return new WordDocument();
}
}
抽象クラスとして、DocumentCreator
クラスがあり、こやつがCreateDocument
を定義しています。
このメソッドが今回の主役であります Factory Method になるわけです。
各ドキュメントのファクトリークラスはこの抽象クラスを継承し、CreateDocument
メソッドをオーバーライドして自らの担当するドキュメントクラスのインスタンスを作成しやす。
(WordCreator
クラスのCreateDocument
で作成するのはWordDocument
インスタンスだし、PDFCreator
クラスのCreateDocument
で作成するのはPDFDocument
インスタンスってことね。)
ここらへんが、先ほどの説明の
スーパークラスでオブジェクトを作成するためのインターフェースが決まっています。 しかし、 サブクラスでは作成されるオブジェクトの型を変更することができます。
を言っているんだよねきっと。
つまり、コード例で言い換えると
スーパークラスでオブジェクトを作成するためのインターフェースが決まっています
⇒ 抽象クラスDocumentCreator
クラスで、ドキュメントクラス(IDocument
を実装したクラス)を作成するための CreateDocument
を定義 している
サブクラスでは作成されるオブジェクトの型を変更することができます
⇒ DocumentCreator
を継承した XXXDocumentCreator
クラスでは、作成するオブジェクトの型はそれぞれ自由( PDFDocument
や WordDocument
)に変更できる
クライアントコード
つぎに、ファクトリークラスの Factory Method (= CreateDocument
)を実際に利用する箇所です。界隈ではクライアントコードと呼ばれます。
public static void Main() {
DocumentCreator[] creators = { new PDFCreator(), new WordCreator() };
foreach (var creator in creators) {
IDocument doc = creator.CreateDocument();
ClientCode(doc);
}
}
public static void ClientCode(IDocument document) {
document.Open();
document.Close();
}
ここまでが Factory Method パターンの解説でよく見るコードです。
こうすると何がいいの?
この構造の利点は、クラス間を疎結合にし、拡張性を向上させるという点です。
具体的に言いますと、クライアントコードが IDocument
インターフェースと DocumentCreator
クラスを通じて操作されている点ですね。
つまり、クライアントコードは具体的なドキュメントクラス(PDFDocument
や WordDocument
)を直接参照していません。
なもんで、例えば新しいドキュメントをシステムで扱うことになった場合、新しいドキュメントクラスとそれに対応する DocumentCreator
クラスを追加するだけでよく、 クライアントコードには修正が要らない ということなんですねー。
これらを総じて 拡張性が高いと言えるわけですはい。
ちょっとだけこんがらがってきたんで具体的にコードで見てみましょう。
新しいドキュメントのプロダクトクラスとファクトリークラスを追加
新しいドキュメントタイプとしてExcelを扱うことになったとします。
前述したとおり、新しいドキュメントクラスとそれに対応する DocumentCreator
クラスを追加しましょう。
// 新しいドキュメントタイプを定義
public class ExcelDocument : IDocument {
public void Open() {
Console.WriteLine("Excelを開きました");
}
public void Close() {
Console.WriteLine("Excelを閉じました");
}
}
// 新しいドキュメントタイプに対応するDocumentCreatorのサブクラスを定義
public class ExcelCreator : DocumentCreator {
public override IDocument CreateDocument() {
return new ExcelDocument();
}
}
これら2つのクラスを追加するだけでよく、クライアントコードについては修正が不要です。
public static void Main() {
// ここでExcelCreatorを追加するだけ
DocumentCreator[] creators = { new PDFCreator(), new WordCreator(), new ExcelCreator() };
foreach (var creator in creators) {
IDocument doc = creator.CreateDocument();
ClientCode(doc);
}
}
// このメソッドに修正は不要
public static void ClientCode(IDocument document) {
document.Open();
document.Close();
}
ごらんのとおりクライアントコードには修正が入っていません。
※ 今回のコード例では Main関数自体は新規に追加したファクトリークラスを利用するための修正は必要です。実際にドキュメントクラスを利用してメインの処理をする ClientCode
メソッドに修正が不要なことに注目してください。
ほほー。確かにこの構造だと、今後扱うドキュメントがどんどん増えていっても、ドキュメントを扱うクライアントコードの修正は不要なので、拡張性が高いコードと言えそうです。
Factory Method って必要?
いや、拡張性が高いってのはすごーく理解できるんですよ。ただ、1つ腑に落ちていない点があって、
これって各ドキュメントクラスが IDocument
インターフェースに実装していることによるポリモーフィズムの恩恵をうけているだけなんじゃあないの?ってことです。
「新しいドキュメントを利用することになっても、クライアントコードには修正が不要」の利点と、Factory Method が存在する理由って直接的に関係してないと思うんですよ。
具体的にコードで示すならば、こんな感じです。
public static void Main() {
// Productインスタンスを直接生成する
Document[] documents = { new PDFDocument(), new WordDocument(), new ExcelDocument() };
foreach (var document in documents) {
ClientCode(document);
}
}
// ドキュメントクラスが増えても引き続きこのメソッドに修正は不要
public static void ClientCode(IDocument document) {
document.Open();
document.Close();
}
別に Factory Method をもつファクトリークラスなんか無くたって、上記のコードでもクライアントコードの修正は不要なんじゃあないの??
なんなら、新しいドキュメントが増えた時、追加するクラスがドキュメントクラスだけでよくて、ファクトリークラスは作らなくて済むので良いことしかないのでは??
じゃあなんでファクトリークラスなんかわざわざ用意して Factory Method を定義する必要があるんだよー。教えて偉い人ー。
いろいろ調べた自分なりの解釈
それぞれのクラスの役割
DocumentクラスとDocumentFactoryクラスの役割をおさらいすると、
Documentクラス
- 具体的なドキュメントの振る舞いを定義
- PDFDocument、WordDocument、ExcelDocumentなど、具体的なドキュメントタイプごとに異なる振る舞いを持つ
DocumentCreator クラス
- 「Documentクラスのインスタンスを生成する」ことのみに責任を持つ
- Documentクラスのインスタンス生成をカプセル化する
AがBを利用(参照)している場合、AはBに依存していると言えます。
つまり、先ほどの例のように、クライアントコード内で、直接ドキュメントクラスのインスタンスを生成してしまうと、クライアントコードが各ドキュメントクラスに依存している状態になる んですね。
依存関係があるということは、依存する側(クライアントコード)は、依存される側(各Documentクラス)の変更に影響を受けてしまうってことです。
将来的にDocumentクラスの実装が変更されると、クライアントコードも修正する必要が出てくるから、これはリスクだよね、というお話です。
なぜ「Documentクラスのインスタンス生成をカプセル化する」必要があるのか。これは 「具体的なDocumentクラスの実装からクライアントコードを隔離することでDocumentクラスに依存させない」 ってことなんですね。たぶん。
なるほど。これが Factory Method をもつファクトリークラスが必要な理由なんですねー。
ファクトリークラスに依存するのはイインデスカ?
まてまて。確かにファクトリークラスを作成することによって、クライアントコードが直接 各 Document クラスを参照することは無くなりましたよ。
でもそうなるってえと、クライアントコードは今度は各 ファクトリークラスを参照する( = 依存する)ことになりますが、それはいいんでしょか?
これに関しては、インスタンスの初期化方法に関してもクライアントコードは知る必要はない(というか知るべきではない)からと、解釈しておりやす。
各ファクトリークラスは、各ドキュメントクラスのインスタンス作成ロジックをカプセル化してくれているため、クライアントコードは各ドキュメントクラスインスタンスの作成方法に対して無関心でいられます。 この役割を担っているファクトリークラスに依存するのは、将来的な変更に対してより柔軟に対応するために必要なことなんでしょう。
例えば、WordDocument クラスに修正が入り、インスタンス作成時にフォントやフォントサイズを指定する必要が出たとします。
ほいで、そのフォントやフォントサイズの値は、ある設定ファイルから取得したい場合を考えます。
WordDocument クラスの修正
public class WordDocument : IDocument {
private string _fontName;
private int _fontSize;
// コンストラクタでフォント名、フォントサイズを受け取る
public WordDocument(string fontName, int fontSize)
{
_fontName = fontName;
_fontSize = fontSize;
}
public void Open() {
Console.WriteLine("Wordを開きました");
}
public void Close() {
Console.WriteLine("Wordを閉じました");
}
}
コンストラクタで、フォント名、フォントサイズを受け取るように修正しました。
ファクトリークラスがある場合
public class WordCreator : DocumentCreator {
public override IDocument CreateDocument() {
// フォント名、フォントサイズを取得するロジックが追加
string fontName = // フォント名を設定ファイルから取得
int fontSize = // フォントサイズを設定ファイルから取得
return new WordDocument(fontName, fontSize);
}
}
ファクトリークラスがある場合、ファクトリークラス内にインスタンス初期化ロジックは隠蔽されます。
なので、クライアントコードは修正する必要がありません。
ファクトリークラスがない場合
ファクトリークラスがない場合は、クライアントコードで直接各ドキュメントクラスのインスタンスを生成する必要があります。
public static void Main() {
// フォント名、フォントサイズを取得するロジックが追加
string fontName = // フォント名を設定ファイルから取得
int fontSize = // フォントサイズを設定ファイルから取得
// 取得した値をコンストラクタで指定
Document[] documents = { new PDFDocument(), new WordDocument(fontName, fontSize), new ExcelDocument() };
foreach (var document in documents) {
ClientCode(document);
}
}
public static void ClientCode(IDocument document) {
document.Open();
document.Close();
}
このように、クライアントコードに各ドキュメントクラスインスタンスの初期化のためのロジックが必要になります。
ドキュメントクラスの種類が増えていき、修正が加わる度に、クライアントコードにはドキュメントクラスそれぞれの初期化ロジックを追加することになります。
"作成するドキュメントクラスが○○だったら□□をする" のような if文 にあふれる未来が想像できます。
まとめ
今回は プログラミングデザインパターンのうち、「Factory Method パターン」について、基本的な部分を復習してみました。
復習していまいち利点がわからなかった点についても、ある程度腑に落ちたかなと思います。
Factory Methodパターン の利点は、オブジェクトの生成を抽象化することにより、クラス間の結合度を低減し、システムの拡張性とメンテナンス性を向上させること です。
「Productクラスが追加されたり変更が発生しても、クライアントコードに修正が必要ない」 状態であることが、システムの拡張性とメンテナンス性向上につながるというわけですな。
また少しだけお利口さんになれた気がします。ありがとうございました。
※自分の都合のいいように解釈している点が大いにあるかもしれません。誤った知識や、理解不足な部分などがありましたらご指摘くださると助かります。