本記事ではGoFのデザインパターンのオブジェクトの生成に関するパターンの一つである「Factory Method」パターンを解説します。このパターンを一言で説明するならば、「オブジェクトの作り方を親クラスで定め、具体的な処理をサブクラスで行うことで、オブジェクトの生成方法を柔軟に行うことができるパターン」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。
「Factory Method」パターンとは
先述したように、FactorMethodパターンとはオブジェクトの作り方を親クラスで定め、具体的な処理をサブクラスで行うことで、オブジェクトの生成方法を柔軟に行うことができるパターンです。オブジェクトを生成するためのインターフェースを定義しておきます。このインターフェースにはメソッドの具体的な処理は一切記述しません。インターフェースを継承するサブクラスでメソッドの処理内容を記述します。
「Factory Method」の書き方
ではFactoryMethodの何が便利なのでしょうか。クラスの中にメソッドを用意してそこで全ての処理を行うのでは、何が問題になるのでしょうか。具体例をあげて考えていきましょう。
✖︎ Factory Methodパターンを使用しない例 ✖︎
あなたは英会話の学習アプリをつくることにしました。まずEnglishクラスを用意して、そこに授業のカリキュラムである、リスニング、ライティング、リーディング、スピーキングのメソッドを作って、それぞれのメソッドに処理を追加していきました。コードのほとんどはEnglishクラスにあります。
public class English {
public void listening () {
System.out.println("Let's start listening");
// 英語のリスニングを構成するの処理を書く
}
public void writing () {
System.out.println("Let's start writing");
// 英語のライティングを構成するための処理を書く
}
public void reading () {
System.out.println("Let's start reading");
// 英語のリーディングを構成するための処理を書く
}
}
この英語学習アプリはどんどん伸びていきます。すると、中国語の教材もアプリに入れて欲しいとの要望が出てきました。
コードを追加する必要があります。新しくChineseクラスを作って、Englishクラスの中身を丸々コピペしてメソッドの処理の中身を中国語学習処理に書き換えました。
public class Chinese {
public void listening () {
System.out.println("让我们开始聆听(リスニングを始めましょう)");
// 中国語のリスニングを構成するための処理を書く
}
public void writing () {
System.out.println("让我们开始写作吧(ライティングを始めましょう)");
// 中国語のライティングを構成するための処理を書く
}
public void reading () {
System.out.println("让我们开始阅读吧(リーディングを始めましょう)");
// 中国語のリーディングを構成するための処理を書く
}
}
これらの教材を利用する場合どうなるでしょう。英語だけなら良いですが、教材の言語が増えるほど、オブジェクトを切り替えるためにif文やswitch文で条件分岐の処理を行う必要が出てきます。また下記のようなコードを追加してオブジェクトを生成してしまうと、戻り値がObuject型になっているため教材以外のオブジェクトを返却してしまう可能性も大いにあります。
また返却されたインスタンスのメソッドを利用するときも、教材によって型が違うためメソッド名が同じでも型判定のif文を追加してからメソッドを利用しなければなりません。非常に冗長でめんどくさいです。
public static Object getTeachingMaterials(LanguageType type) {
switch (type) {
case ENGLISH:
return new English();
case CHINESE:
return new Chinese();
case GERMAN:
:
:
:
⭕️ Factory Methodパターンを使用する例 ⭕️
次は将来のアプリの拡張を見据えて、Factory Methodを利用して教材アプリを作っていきます。拡張のイメージとしては、教材それぞれに工場を作ります。工場にはベースとなる設計書があり、教材にも何を行うかの枠組みが用意されています。
まずは英語教材や中国語教材の親クラスとなるLanguageクラスを用意し、教材で利用する抽象メソッドを作ります。
さらにそのLanguageクラスを継承したEnglishクラスとChineseクラスを用意し、抽象メソッドをオーバーライドして、教材の処理を実装します。
// 言語抽象クラス
public interface Language {
void listening();
void writing();
void reading();
}
// 英語教材クラス
public class English implements Language {
@Override
public void listening () {
System.out.println("Let's start listening");
// 英語のリスニングを構成するの処理を書く
}
@Override
public void writing () {
System.out.println("Let's start writing");
// 英語のライティングを構成するための処理を書く
}
@Override
public void reading () {
System.out.println("Let's start reading");
// 英語のリーディングを構成するための処理を書く
}
}
// 中国語教材クラス
public class Chinese implements Language {
@Override
public void listening () {
System.out.println("让我们开始聆听(リスニングを始めましょう)");
// 中国語のリスニングを構成するための処理を書く
}
@Override
public void writing () {
System.out.println("让我们开始写作吧(ライティングを始めましょう)");
// 中国語のライティングを構成するための処理を書く
}
@Override
public void reading () {
System.out.println("让我们开始阅读吧(リーディングを始めましょう)");
// 中国語のリーディングを構成するための処理を書く
}
}
上記で定義したクラスからオブジェクトを生成するためのクラスを用意します。FactoryMethodでは生成したい製品(ここでは言語教材)ごとに専用の工場(Factory)を立てて、工場の中でオブジェクトを生成します。利用者側は工場のオブジェクトのみを生成します。
工場のオブジェクトの型が製品によって異なると、言語教材に抽象クラスを継承した意味がなくなります。そこで工場にも親となる抽象クラスを用意して、クラスの初期化を行うメソッドを抽象メソッドで定義します。この工場の抽象メソッドを継承して製品ごとの工場を作成します。
// 言語教材の抽象クラス
public abstract class Factory {
public final Language create() {
return createLanguage();
}
protected abstract Language createLanguage();
}
// 英語教材工場クラス
public class EnglishFactory extends Factory {
@Override
protected Language createLanguage() {
return new English();
}
}
// 中国語教材工場クラス
public class ChineseFactory extends Factory {
@Override
protected Language createLanguage() {
return new Chinese();
}
}
FactoryクラスのcreateLanguage()が抽象メソッドとなり、それぞれの工場で教材のインスタンスを生成します。今回はオブジェクトを生成しているだけですが、オブジェクト生成に必要なパラメータやコンストラクタに設定する引数等を、継承したクラス内のcreateLanguage()メソッド内で行うと良いでしょう。
次に作成した言語教材工場クラスを利用して、教材オブジェクトを生成します。
public class Main {
public static void main(String[] args) {
Factory factory = createFactory(LanguageType.ENGLISH);
Language english = factory.create();
english.listening();
english.writing();
english.reading();
}
// 与えられたenumの値から生成する工場を変更する
public static Factory createFactory(LanguageType type) {
return switch (type) {
case ENGLISH -> new EnglishFactory();
case CHINESE -> new ChineseFactory();
default -> throw new IllegalArgumentException("指定されたtypeの教材はありません");
};
}
}
// 教材の言語タイプ
public enum LanguageType {
ENGLISH,
CHINESE,
GERMAN,
}
今回の例ではcreateFactoryを静的メソッドで作成しました。LanguageTypeを引数に指定することで、それに応じたFactoryのオブジェクトを返却します。このメソッドで注目して欲しいのが、戻り値の型がFactory型になっていることです。先程の例ではObject型で返却していました。それぞれの言語教材工場のオブジェクトはFactoryを継承しているため、戻り値もFactory型でまとめることができます。
利用者側はFactoryが何の教材の工場かは引数で指定するだけで、型の意識をする必要がありません。またfactory.create()で生成される教材も何の教材かを意識する必要がないため、非常に疎結合なコードであり、言語教材オブジェクトを柔軟に生成することを可能としています。
「Factory Method」と「Abstract Factory」の違い
オブジェクトの生成には似たようなもので「Abstract Factory」があります。混同しがちですが、これらのパターンには大きな違いがあります。
Abstract Factory | 関連したオブジェクトの集まりを、具象クラスを指定しなくても生成することが可能になるパターン ■メリット ・オブジェクト群の生成を別のオブジェクト群に変更する場合、具象クラスはみていないため、Factoryを丸ごと変更することで利用元への影響を抑えることができる ■デメリット ・オブジェクトの数が増えたり、オブジェクトの抽象クラスが変更してしまう場合などにはFactoryを使い回せない ・Factoryを経由して、オブジェクト群を含むオブジェクトを生成するため、コードが複雑になる可能性がある |
Factory Method | オブジェクトの作り方を親クラスで定め、具体的な処理をサブクラスで行うことで、オブジェクトの生成方法を柔軟に行うことができるパターン ■メリット ・オブジェクトのインターフェースがあるので、具象クラスのメソッド具象クラスごとに柔軟に変更できる ■デメリット ・インターフェースを実装した具象クラスを増やすごとにFactoryクラスを増やす必要があるため、コードが複雑になる可能性がある |
どちらにもメリット・デメリットがあるため、お互いの特徴を理解しつつ、単一のオブジェクトの抽象化には「Factory Method」を利用し、オブジェクト群の生成に関する抽象化の場合は「Abstract Factory」を利用すると良いでしょう。「Abstract Factory」の解説は下記の記事で解説しています。
まとめ
本記事で利用した「Factory Method」のクラス図は下記になります。
「Factory Method」はオブジェクトのインターフェースを決めて、メソッドを抽象化するパターンです。是非利用してみて下さい。
本記事で利用したコードは下記で公開しております。
コメント