こんにちはecbeingでアーキテクトをやっている宮原です。
皆さんデザインパターンについてはどのくらいご存知でしょうか?
「かなり自信がある」という方も「名前は聞いたことあるけど・・・」という方もいると思います。
今日はそんなデザインパターンの中から「Factory Method」を取り上げます。
Factory Methodとは?
さて、Factory Methodといえばデザインパターンの中でもかなり有名なパターンであり、
デザインパターンに自信がない方でも名前だけは目にしたことがあるという方も多いのではないでしょうか?
このパターンは「Virtual Constructor」とも呼ばれ、コンストラクタの代わりになるようなメソッドを作ることがキモなパターンです。
コンストラクタの代わりにインスタンスの工場(Factory)となるメソッド(Method)を作るから「FactoryMethod」パターンなんですね。
これだけ聞くと随分と簡単そうな気がするこの「Factory Method」ですが、
物の本やサイトなどを読みますと打って変わって難しいことが書かれています。
例としてデザインパターンのバイブルでもある「オブジェクト指向における再利用のためのデザインパターン(通称:デザパタ本)」の説明を見てみましょう。
オブジェクトを生成するときのインタフェースだけを規定して、実際にどのクラスをインスタンス化するかはサブクラスが決めるようにする。
Factory Methodパターンは、インスタンス化をサブクラスに任せる。
(オブジェクト指向における再利用のためのデザインパターン p.115 より)
何やら難しい事を言ってます。
なんでサブクラスの話が出てくるんでしょうか?
コンストラクタの代わりにメソッドを作るだけですよね??
さらにWikipediaの説明も見てみましょう。
Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な
自分のメソッドに置き換えることで、 アプリケーションに特化したインスタンスの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。
(Wikipedia 「Factory Method パターン」より)
なるほど、ますますわからなくなりました。
実はこれらの説明は「Factory Methodを使う理由」から由来するものなのです。
割とよくあるFactory Methodの使い方としては「生成オブジェクトのパラメータ化」というようなものがあります。
これは皆さんもご覧になったことがあるのではないでしょうか?
【サンプルコード:生成オブジェクトのパラメータ化】
using System; var c1 = FactoryMethod("01"); var c2 = FactoryMethod("02"); var c3 = FactoryMethod("03"); Console.WriteLine(c1); Console.WriteLine(c2); Console.WriteLine(c3); Parent FactoryMethod(string typeName) { switch (typeName) { case "01": return new Child_01(); case "02": return new Child_02(); case "03": return new Child_03(); default: throw new Exception(); } } class Parent { } class Child_01 : Parent { } class Child_02 : Parent { } class Child_03 : Parent { }
こんな感じで、「factoryMethod()」にわたす引数によって、
生成オブジェクトを切り替える、という使い方です。
この利用方法もFactory Method を使う理由の一つですが、実はもう一つ「具象クラスを書かずにインスタンスを生成する」という非常に重要な理由があります。
なぜ具象クラスを書かずにインスタンスを生成することが重要なのでしょうか?
この理由について説明したいと思います。
その1:単純な「FactoryMethod()」
では、最も単純に「インスタンスを生成するメソッド」を作ってみましょう。
【サンプルコードその1】
Product product = FactoryMethod(); Product FactoryMethod() { return new Product(); } class Product { }
こんな感じになりますね。
「生成オブジェクトのパラメータ化」すらしていないので、これだけだとメリットが全くありません。
また、細かいですが関数がトップレベルで書かれています。
通常のオブジェクト指向なコードだと実行コードはクラス内部に書かれるので、まずはそのようにしてみましょう。
その2:オブジェクト指向前提な「FactoryMethod()」
【サンプルコードその2】
class Program { static void Main(string[] args) { Product product = FactoryMethod(); } static Product FactoryMethod() { return new Product(); } } class Product { }
さて上記のコードですが、いかにもサンプルコード然としています。どこがサンプルコードっぽいのかと言うと、Main()の中で「FactoryMethod()」を使ってしまっているところですね。
まあ実際サンプルコードなので当然なのですが、このままだとFactory Method を使う理由の「具象クラスを書かずにインスタンスを生成する」のメリットが実感できません。
なので、「FactoryMethod()」を別に作成するLogicクラスから使用することにしましょう。
その3:Logicクラスから呼ばれる「FactoryMethod()」
【サンプルコードその3】
class Program { static void Main(string[] args) { Logic logic = new Logic(); logic.Do(); } } class Logic { public void Do() { Product product = FactoryMethod(); } static Product FactoryMethod() { return new Product(); } } class Product { }
Logicクラスを作って、そこの「Do()」が主処理を行うコードを作成しました。
この場合、Main()の仕事はLogicクラスの生成とDo()のキックだけです。
こうなって初めて「具象クラスを書かずにインスタンスを生成する」というメリットを理解するための土台が出来ました。
この「具象クラスを書かずにインスタンスを生成する」の「書かずに」という部分は「誰が」書かずにすむのか、という説明をしてませんでしたよね?
これはMain()やProgramクラスではありません。
このLogicクラスの中に具象クラスを書かずにすむ、ということなのです。
ご覧の通りこのままではLogicクラス内にProductクラスがバッチリと書かれていますね。
FactoryMethod()内でコンストラクタを呼んでいるのでこれは当然といえば当然です。
ということはどういうことでしょう?
FactoryMethod()はLogicクラス内にあってはダメだということです。
ではFactoryMethod()を別クラスに移動しましょう。
その4:Creatorクラスにある「FactoryMethod()」
【サンプルコードその4】
class Program { static void Main(string[] args) { Logic logic = new Logic(); logic.Do(); } } class Logic { public void Do() { Creator creator = new Creator(); IProduct product = creator.FactoryMethod(); } } class Creator { public IProduct FactoryMethod() { return new Product(); } } interface IProduct { } class Product : IProduct { }
上記コードの工夫点は2つです。
「FactoryMethod()」をCreatorクラスに移動したため、Productクラスのコンストラクタ呼び出しをLogicクラスから消すことが出来ました。
そしてさらに「FactoryMethod()」の返り値の型をProductクラスからIProductインタフェースに変更したので、ProductクラスをLogicクラスから消すことが出来ました!
・・・さて読者の皆様におかれましては2つほどツッコみたい気持ちを我慢されていることでしょう。
お気持ちはよくわかりますが、落ち着いて私の説明を聞いて下さい。
まず、1つ目「LogicクラスからProductクラスは無くなったけどIProductインタフェースに変わっただけじゃないか!」
というご指摘ですが、もう一度Factory Methodを使う理由を思い出してください。
「具象クラスを書かずにインスタンスを生成する」 |
インタフェースは具象クラスじゃないから良いのです。
実のところデザインパターンやオブジェクト指向のテクニックのアレコレは、突き詰めると具象クラスの代わりにインタフェースを使ったり、抽象クラスを使ったりすることが基本なのです。
従って「具象クラスを書かずにインスタンスを生成する」ためにFactory Methodパターンが実際何をするかと言うと、「具象クラスの代わりにインタフェースや抽象クラスを使ってインスタンス生成する」といった事を行うのです。
そしてもう1つのツッコミ「Creatorクラス書いてるじゃないの!!」というご指摘。
いやあ、さすがお目が高い。
そうです、「FactoryMethod()」をCreatorクラスに追い出したことでせっかくProductクラスの記述がLogicクラスから無くなりましたが、今度はCreatorクラスの記述が増えてしまいました。
ではこれを解消するためにCreatorクラスをLogicクラスから追い出し、新たに別のクラスを作って、そこにCreatorクラスのインスタンスを作るため「FactoryMethod()」を作りましょうか?
もちろんそんなことしても堂々巡りになるだけです。
このジレンマを解消するための一手が「依存性の注入」です。
その5:依存性の注入でCreatorクラスを生成
【サンプルコードその5】
class Program { static void Main(string[] args) { Logic logic = new Logic(new Creator()); logic.Do(); } } class Logic { private ICreator creator; public Logic(ICreator creator) { this.creator = creator; } public void Do() { IProduct product = creator.FactoryMethod(); } } interface ICreator { IProduct FactoryMethod(); } class Creator : ICreator { public IProduct FactoryMethod() { return new Product(); } } interface IProduct { } class Product : IProduct { }
上記コードをご覧ください。
Logicクラス内に書かれていたCreatorクラスのコンストラクタ呼び出しはMain()に移り、
Logicクラスでは渡されたインスタンスをICreatorインタフェース経由で使用するのみとなっています。
この様にあるクラス内で使用するオブジェクトのインスタンスをクラス外で生成することを「依存性の注入」と呼びます。
「依存性」の「依存」とは参照すること、つまりクラスを書くことです。
通常であれば具象クラスに依存しなければそのインスタンスを入手することは出来ませんが、クラスの外でインスタンスを作成し、それを注入(インスタンスを貰うこと)することで、具象クラスに依存せずそのインスタンスのみを使用することが出来ます。
ちなみにFactory Method の利点である「具象クラスを書かずにインスタンスを生成する」という文章を「依存」で置き換えると、「具象クラスに依存せずにインスタンスを生成する」ということになります。
オブジェクト指向プログラミングを行う際には「依存」という言葉が非常によく出てくるのでぜひ覚えておいて下さい。
ここからは「クラスを書く」という言葉を「依存する」という言葉に置き換えて解説していきます。
もう一度上記コードのLogicクラスを御覧ください。
クラス内に書かれていたCreatorクラスはICreatorインタフェースに代替され、Logicクラスはついに具象クラスに依存しなくなりました。
この状態こそが「Factory Method」を使う最大の理由である、「具象クラスに依存せずにインスタンスを生成」している状態です。
その6:なぜ依存してはいけないのか?
しかしなぜここまでして具象クラスをに依存してはいけないのでしょう?
全ては「上位モジュールは下位モジュールに依存してはならない」というオブジェクト指向の原則に従うためです。
この原則のことを「Dependency Inversion Principle(DIP:依存性逆転の原則)」といいます。
この原則をもう少し詳しく見ていきましょう。
モジュールとはやや曖昧な用語であり、時にクラスだったりファイルだったりDLLだったりします。
今回はモジュール=名前空間として以下のような構成にします。
- 上位モジュール:名前空間[Business]
- Logic クラス
- ICreator インターフェイス
- IProduct インターフェイス
- 下位モジュール:名前空間[Main]
- Program クラス
- Creator クラス
- Product クラス
この様にモジュールを分けた構成にコードを変更します。
【サンプルコードその6】
namespace Main { using Business; class Program { static void Main(string[] args) { Logic logic = new Logic(new Creator()); logic.Do(); } } class Creator : ICreator { public IProduct FactoryMethod() { return new Product(); } } class Product : IProduct { } } namespace Business { class Logic { private ICreator creator; public Logic(ICreator creator) { this.creator = creator; } public void Do() { IProduct product = creator.FactoryMethod(); } } interface ICreator { IProduct FactoryMethod(); } interface IProduct { } }
上位モジュールに属するLogicクラスは、下位モジュールに属するCreatorクラスに依存していません。
代わりにICreatorインタフェースに依存しています。
しかしICreatorはLogicと同じ上位モジュールに属するため、「上位モジュールから下位モジュールに依存してはならない」という原則に従っています。
上位モジュールとは「使う/使われる」関係で表現すると「使う」側のモジュールです。
通常使う側のモジュール内で、使われる側の下位モジュールの具象クラスを使おうとした場合、その具象クラスのコンストラクタを使用する必要があります。
しかしコンストラクタを使用してオブジェクトを利用する場合、依存方向は利用方向と一致するため「上位モジュールから下位モジュールに依存してはならない」という原則に反してしまいます。
【コンストラクタを使用した場合の依存関係】
ところがサンプルコードその6の構造を表した下記の図を御覧ください。
【サンプルコードその6の構成図】
利用方向と依存方向が逆になっているのがお分かりでしょうか?これこそが「依存性の逆転」なのです。
「具象クラスに依存せずにインスタンスを生成する」ことが重要な理由はこの「依存性の逆転」を実現できることにあったのです。
Factory Methodパターンのメリットとデメリット
Factory Methodの最大のメリットは「依存性の逆転」が実現できることです。
また、もう一つのメリットとして「生成オブジェクトのパラメータ化」があることは冒頭で説明しました。
では「依存性の逆転」をすることのメリットもここでご紹介しておきましょう*1。
「依存性の逆転」のメリット
- 下位モジュールの実装を交換できる
- 上位モジュール実装時に下位モジュールの詳細を気にしなくても良い
- 下位モジュールが完成していなくても上位モジュールのテストが出来る
- 下位モジュールの変更に上位モジュールが影響を受けない
しかしFactory Methodパターンにもデメリットはあります。
皆さんすでにお気づきと思いますが、デメリットは下記のとおりです。
Factory Methodパターンのデメリット
- コード量が多くなる
メリットに比べてデメリットが少ないように感じますが、このたった一つのデメリットが数多いメリットより勝る場合があります。
コンストラクタが使えれば1行で済む処理を、Factory Methodパターンを採用したおかげで、数十行も余計にコーディングしなければなりません。
さらに言えば下記のようにコンストラクタを使う場合よりも型が3つも増えてしまっています。
- Creatorクラス
- ICreatorインタフェース
- IProductインタフェース
このデメリットを軽視することは危険です。
KISSの原則*2はオブジェクト指向プログラミングにも当然当てはまる原則なのです。
従ってFactory Methodパターンを(さらにいえば依存性の逆転を)使用する際はそれが本当に必要なのかをよく検討してから利用することが大切です。
例えば「FactoryMethod()」をstaticメソッドにして、Factory Methodパターンを部分的に採用する、などは後々完全なFactory Methodパターンの適用が必要な場合に対応が簡単になります。
全てのコンストラクタを撲滅しようと躍起になってはいけません。
必要な時に必要なだけパターンを利用することが重要です。
Factory Methodパターンの使い所
では Factory Methodの具体的な使い所について説明します。
まず依存性を逆転させた上で動的なオブジェクト生成*3を行う際にご使用下さい。
またFactory MethodはAbstract FactoryやTemplate Methodなど他のデザインパターンから利用されることが多いパターンですので、それらのパターンを利用する際は同時にFactory Methodも使うことになるでしょう。
Factory Methodパターンを常に使用する必要が無いのと同様に、「依存性逆転の原則」も常に守る必要があるわけではないことも覚えておいて下さい。
「依存性の逆転」はロジックの抽象化度を高く保ちたい場合、利用する具象クラスが頻繁に変更される可能性が高い場合など、限定された状況で使用するべきでしょう。
何度もいいますが、これらのパターンや原則は本当に必要なのかをよく見極めてから使う必要があります。
以上、Factory Methodパターンの説明はなぜあんなに難しいのか、というお話でした。
ecbeingでは技術の使い所を見極められるエンジニアを募集しています!!
careers.ecbeing.tech