🏭

デザインパターン: Factory Methodパターン -インスタンス作成をサブクラスにまかせる-

2024/05/25に公開

はじめに

GoFの23のデザインパターンがまとめられている「Java言語で学ぶデザインパターン入門」を読んでアウトプットとしてデザインパターンを1つずつ記事としてアウトプットしていきます。
原則的にJavaで実装コード例などを記述していきますが、気になったことや改善点、感想等ありましたらぜひコメントくださると嬉しいです!

23のパターン一覧はこちらから ※随時更新中

Factory Methodパターンとは


Factory Methodパターンのイメージ
生成に関するデザインパターンの一つで、 スーパークラスでオブジェクトを作成するためのインターフェースが決まっています。 しかし、 サブクラスでは作成されるオブジェクトの型を変更することができます。
これによりスーパークラスとサブクラスのオブジェクト間の疎結合を実現します。

Template Methodパターンとの関係

Factory MethodパターンはTemplate Methodパターンをオブジェクト生成の場面に適応させたデザインパターンと言えます。
簡単に言えば、オブジェクト生成を容易にするデザインパターンです。

実装例

Factory Methodパターンを使用しない例

Appleは革新的な技術を持ってiPhoneをつくることにしました。まずデバイスクラスを用意して、そこにデバイスの機能である「起動」「操作」「停止」のメソッドを作って、それぞれのメソッドに処理を追加していきました。コードのほとんどはiPhoneクラスにあります。

IPhoneクラス
public class IPhone {
    public void boot () {
        System.out.println("Let's boot iPhone");
        // iPhoneを起動するための処理を書く
    }
    public void operate() {
        System.out.println("Let's operate iPhone");
        // iphoneを操作するための処理を書く
    }
    public void stop () {
        System.out.println("Let's stop iPhone");
        // iPhoneを停止するための処理を書く
    }
}

Appleの販売したiPhoneの売上はどんどん伸びていきます。すると、他のデバイスも製造販売したいとの要望が出てきました。

コードを追加する必要があります。新しくIPadクラスを作って、IPhoneクラスの中身を丸々コピペしてメソッドの処理の中身をIPad処理に書き換えました。

IPadクラス
public class IPad {
    public void boot () {
        System.out.println("Let's boot iPad");
        // iPadを起動するための処理を書く
    }
    public void operate () {
        System.out.println("Let's operate iPad");
        // iPadを操作するための処理を書く
    }
    public void stop () {
        System.out.println("Let's stop iPad");
        // iPadを停止するための処理を書く
    }
}

これらのデバイスを利用する場合どうなるでしょう。iPhoneだけなら良いですが、デバイスの種類が増えるほど、オブジェクトを切り替えるためにif文やswitch文で条件分岐の処理を行う必要が出てきます。また下記のようなコードを追加してオブジェクトを生成してしまうと、戻り値がObuject型になっているためデバイス以外のオブジェクトを返却してしまう可能性も大いにあります。
 
また返却されたインスタンスのメソッドを利用するときも、デバイスによって型が違うためメソッド名が同じでも型判定のif文を追加してからメソッドを利用しなければなりません。非常に冗長でめんどくさいです。

public static Object get DeviceMaterials(DeviceType type) {
    switch (type) {
        case IPHONE: 
            return new IPhone();
        case IPAD:
            return new IPad();
        case MACBOOK:
        :
        :
        :

Factory Methodパターンを使用する例

次は将来の拡張を見据えて、Factory Methodを利用してデバイスを作っていきます。拡張のイメージとしては、デバイスそれぞれに工場を作ります。工場にはベースとなる設計書があり、デバイスにも何を行うかの枠組みが用意されています。

 まずはiPhoneやiPadの親クラスとなるDeviceクラスを用意し、デバイスで利用する抽象メソッドを作ります。

さらにそのDeviceクラスを継承したIPhoneクラスとIPadクラスを用意し、抽象メソッドをオーバーライドして、デバイスの処理を実装します。

// 言語抽象クラス
public interface Device {
    void boot();
    void operate();
    void stop();
}

// IPhoneクラス
public class IPhone implements Device {
    @Override
    public void boot () {
        System.out.println("Let's boot iPhone");
        // iPhoneを起動するための処理を書く
    }
    @Override
    public void operate () {
        System.out.println("Let's operate iPhone");
        // iphoneを操作するための処理を書く
    }
    @Override
    public void stop () {
        System.out.println("Let's stop iPhone");
        // iPhoneを停止するための処理を書く
    }
}

// IPadクラス
public class IPad implements Device {
    @Override
    public void boot () {
        System.out.println("Let's boot iPad");
        // iPadを起動するための処理を書く
    }
    @Override
    public void operate () {
        System.out.println("Let's operate iPad");
        // iPadを操作するための処理を書く
    }
    @Override
    public void stop () {
        System.out.println("Let's stop iPad");
        // iPadを停止するための処理を書く
    }
}

上記で定義したクラスからオブジェクトを生成するためのクラスを用意します。FactoryMethodでは生成したい製品(ここではデバイス)ごとに専用の工場(Factory)を立てて、工場の中でオブジェクトを生成します。利用者側は工場のオブジェクトのみを生成します。

工場のオブジェクトの型が製品によって異なると、デバイスに抽象クラスを継承した意味がなくなります。そこで工場にも親となる抽象クラスを用意して、クラスの初期化を行うメソッドを抽象メソッドで定義します。この工場の抽象メソッドを継承して製品ごとの工場を作成します。

// デバイスの抽象クラス
public abstract class Factory {
    public final Device create() {
        return create Device();
    }
    protected abstract Device createDevice();
}

// iPhone工場クラス
public class IPhoneFactory extends Factory {
    @Override
    protected Device createDevice() {
        return new IPhone();
    }
}

// iPad工場クラス
public class IPadFactory extends Factory {
    @Override
    protected Device createDevice() {
        return new IPad();
    }
}

FactoryクラスのcreateDevice()が抽象メソッドとなり、それぞれの工場でデバイスのインスタンスを生成します。今回はオブジェクトを生成しているだけですが、オブジェクト生成に必要なパラメータやコンストラクタに設定する引数等を、継承したクラス内のcreateDevice()メソッド内で行うと良いでしょう。

次に作成したデバイス工場クラスを利用して、デバイスオブジェクトを生成します。

public class Main {
    public static void main(String[] args) {
        Factory factory = createFactory(DeviceType.IPHONE);
        Device iPhone = factory.create();
        iPhone.boot();
        iPhone.operate();
        iPhone.stop();
    }

    // 与えられたenumの値から生成する工場を変更する
    public static Factory createFactory(DeviceType type) {
        return switch (type) {
            case IPHONE -> new IPhoneFactory();
            case IPAD -> new IPadFactory();
            default -> throw new IllegalArgumentException("指定されたtypeのデバイスはありません");
        };
    }
}

// デバイスのタイプ
public enum DeviceType {
    IPHONE,
    IPAD,
    MACBOOK,
}

今回の例ではcreateFactoryを静的メソッドで作成しました。DeviceTypeを引数に指定することで、それに応じたFactoryのオブジェクトを返却します。このメソッドで注目して欲しいのが、戻り値の型がFactory型になっていることです。先程の例ではObject型で返却していました。それぞれのデバイス工場のオブジェクトはFactoryを継承しているため、戻り値もFactory型でまとめることができます。

利用者側はFactoryが何のデバイスの工場かは引数で指定するだけで、型の意識をする必要がありません。またfactory.create()で生成されるデバイスも何のデバイスかを意識する必要がないため、非常に疎結合なコードであり、デバイスオブジェクトを柔軟に生成することを可能としています。

Factory Methodパターンのメリデメ

メリット

  • オブジェクトのインターフェースがあるので、具象クラスのメソッドを具象クラスごとに柔軟に変更できる

デメリット

  • インターフェースを実装した具象クラスを増やすごとにFactoryクラスを増やす必要があるため、コードが複雑になる可能性がある

まとめ

Factory Methodパターンとは

**オブジェクトの作り方を親クラスで定め、具体的な処理をサブクラスで行うことで、オブジェクトの生成方法を柔軟に行うことができるパターン
**です。

本記事で利用した「Factory Method」のクラス図は下記になります。

「Factory Method」はオブジェクトのインターフェースを決めて、メソッドを抽象化するパターンです。是非利用してみて下さい。

Discussion