PHPでFactory Method(ファクトリーメソッド)を学ぶ【デザインパターン】

PHP
※当サイトはアフィリエイト広告を掲載しています。

デザインパターンは、コードの保守や拡張性を高めるための定番として、全23の手法があります。

その中のオブジェクト生成に関するパターンの一つが、Factory Method(ファクトリーメソッド)です。※以後、Factory MethodまたはFactory Methodパターンと表記します。

概要は「オブジェクトの生成をサブクラスに委譲するデザインパターン」です。

本ページではPHPを利用して、Factory Methodの基本的な概念~実装方法についてわかりやすく解説します。

Lara
Lara

私もデザインパターンは一つずつ改めて学びながらまとめています。間違いなどがあればご指摘ください!

Factory Methodパターンとは?

概要

オブジェクトの生成は、通常newキーワードを使って必要な際に行います。

// newして雑誌オブジェクトを生成
$magazine = new Magazine();

対してFactory Methodは、自分でnewせずに、サブクラスに委譲させます。

// factory(工場)をnewして、そのオブジェクトにオブジェクト生成を委譲する
$magazineFactory = new MagazineFactory();
$magazine = $magazineFactory->createBook();

一見、回りくどく、面倒なことをしているように思えるかもしれません。

しかしこのデザインパターンを使用することで、コードの柔軟性を高め、システムの拡張や変更が容易にすることが可能です。

Lara
Lara

なぜこれが便利になるかは、これからのコード例を通じてご紹介出来ればと思います。

「Factory」という言葉は、英語で「工場・製造所」。「method」は「方法・手続き」を意味します。オブジェクトを生成する工場を戦略的に使うパターンという感じですね。

メリット

先ほどの通り、Factory Methodパターンを使用すると、具体的なクラスのインスタンス生成をサブクラスに委ねることができます。

これにより、以下の様なメリットがあります。

  • 柔軟性がある:
    アプリケーションが必要とするオブジェクトの具体的な型を固定せずに、サブクラスでその生成方法を定義可能です。そのため新しいオブジェクトの型を簡単に追加することができます。
  • 疎結合にできる:
    クライアントと具体的なクラスを分離します。そのためシステムの一部を変更するときに、他の部分への影響を最小限に抑えることができます。
  • コードの再利用性が高い:
    既存のコードを新しい状況やタスクに再利用するのが容易になります。
  • 単一責任の原則を守れる:
    オブジェクトの生成とその使用を分離することで、各クラスはそれぞれの責任に集中することができます。周知の通り、これはオブジェクト指向プログラミングの基本です。

デメリット

他のデザインパターンと同様、当然デメリットもあります。

  • コードが増え複雑になる:
    オブジェクトの生成方法を抽象化するため、コードがそれなりに大きく・複雑になります。シンプルなシステムではオーバーエンジニアリング(必要以上に複雑で非効率)になることもあります。
  • 多くのクラスができる:
    専用のFactoryクラスをそれぞれ作成する必要があります。これにより、システム内のクラスの数が増加します。
  • 初心者に難しい:
    複雑になるということは、初心者にはこのパターンの構造が学びにくい&とっつきにくく感じるかもしれません。初心者にとっては素直にnewした方が単純明快です。
Lara
Lara

文章中心の解説はここまで!これからコードを元に解説していきますので、何となくの理解のままでも読み進めてください。

書籍情報を取得するコード例からFactory Methodを学ぶ

Factory Methodは、これまでご紹介してきたSingletonPrototypeよりもコードが長く理解しづらくなるため、最初から簡単な実例として挙げます。

内容は「書籍のジャンルごとに情報を取得する」というコード例ですが、3つのパートに分けます。

  1. 書籍ジャンルの定義
  2. 書籍ジャンルのファクトリ(製造工場)
  3. コードの実行(実際に1, 2を使用)

それでは順番に見ていきましょう。

書籍ジャンルの定義

書籍ジャンルとして、小説Novelクラスと雑誌Magazineクラスを考えました。共通のメソッドを持たせたいので、interfaceであるBookをそれぞれ実装しています。

<?php
// 書籍ジャンルインターフェース
interface Book {
    public function getDescription(): string;
}

// 具体的な書籍ジャンル: 小説
class Novel implements Book {
    public function getDescription(): string {
        return "これは小説です。";
    }
}

// 具体的な書籍ジャンル: 雑誌
class Magazine implements Book {
    public function getDescription(): string {
        return "これは雑誌です。";
    }
}

書籍ジャンルのファクトリ(製造工場)

次に、先ほど作った書籍のジャンルクラスのオブジェクトを作成するファクトリを作ります。ファクトリとは、クラスを作り出す製造工場と考えてください。

ここでも共通のメソッドを実装させるために、抽象クラスであるBookFactoryクラスを作り、NovelFactoryMagazineFactoryというファクトリに継承させました。

<?php
// 書籍ジャンルのファクトリの抽象クラス
abstract class BookFactory {
    abstract public function createBook(): Book;
}

// 小説のファクトリ
class NovelFactory extends BookFactory {
    public function createBook(): Book {
        return new Novel();
    }
}

// 雑誌のファクトリ
class MagazineFactory extends BookFactory {
    public function createBook(): Book {
        return new Magazine();
    }
}

この例では抽象クラスではなく、interfaceでも同じく動作させられます。interfaceやabstractは(使い分けは難しいですが)必要に応じて使います。いずれにしても、このような“プログラムの型一式”をもってFactory Methodというパターンになるものと私は理解しました。

コードの実行

これまでのコードを利用し、クラスを取得してみます。

$novelFactory = new NovelFactory();
$novel = $novelFactory->createBook();
echo $novel->getDescription(); // "これは小説です。" を返す

$magazineFactory = new MagazineFactory();
$magazine = $magazineFactory->createBook();
echo $magazine->getDescription(); // "これは雑誌です。" を返す

はい、特に難しいことはありませんね。

しかし、これで「なるほど、これは便利だな!」と思う方はいないでしょう。

むしろ「以下で良くない!?」と思う方もいると思います。

$novel = new Novel();
echo $novel->getDescription(); // "これは小説です。" を返す

$magazine = new Magazine();
echo $magazine->getDescription(); // "これは雑誌です。" を返す

小さなシステムで、これ以上拡張する可能性が無いならそれもいいかもしれません。必要以上に複雑にしてしまうだけになってしまうからです。

Factory Methodパターンの良いところは、サブクラスにオブジェクト生成を委譲することで、利用側はどのようなクラスか知る必要がないところにあります。

次項では、今回作ったFactory Methodパターンのサンプルコードをベースとし、仕様変更が生じた場合を想定していきます。

その中で、少しでもメリットを感じていただければと思います。

仕様変更例-期間限定プロモーション

先ほどのコードに、以下の様な仕様変更が入った場合を考えてみました。

  • 書店がプロモーションを行っており、一定の条件下で、小説の購入者に特別版の小説を提供したい
  • このプロモーションは期間限定であり、終了後は通常の小説を提供するように戻す必要がある。

このシチュエーションを、先ほどのFactory Methodパターンコードを修正して実装する方法を考えます。

コード例

今回は見やすくするためにまとめてコードを挙げます。

背景に薄く色がついている部分が、前回から変更がある部分です。

<?php
interface Book {
    public function getDescription(): string;
}

class Novel implements Book {
    public function getDescription(): string {
        return "これは小説です。";
    }
}

class SpecialEditionNovel implements Book {
    public function getDescription(): string {
        return "これは特別版の小説です。";
    }
}

class Magazine implements Book {
    public function getDescription(): string {
        return "これは雑誌です。";
    }
}

abstract class BookFactory {
    abstract public function createBook(): Book;
}

class NovelFactory extends BookFactory {
    // この値は本来DBに保存された期限から計算するなどを想定
    private $isPromotion = true;

    public function createBook(): Book {
        if ($this->isPromotion) {
            return new SpecialEditionNovel();
        }
        return new Novel();
    }
}

class MagazineFactory extends BookFactory {
    public function createBook(): Book {
        return new Magazine();
    }
}

// プロモーション期間中
$novelFactory = new NovelFactory();
$novel = $novelFactory->createBook();
echo $novel->getDescription(); // "これは特別版の小説です。" を返す

$magazineFactory = new MagazineFactory();
$magazine = $magazineFactory->createBook();
echo $magazine->getDescription(); // "これは雑誌です。" を返す

このコード例では、プロモーションの有無に応じて異なる種類の小説を生成することができます。

オブジェクトの生成ロジックを NovelFactory 内にカプセル化することで、クラスの利用側がプロモーションの詳細や、具体的な小説のクラスを知る必要がありません。

また、プロモーションが終了した後も、利用する側はコードを改修する必要がないこともメリットです。

コード内のコメントにもある通り、実際にはプロモーション期限はデータベース内に登録しておき、期限が過ぎたらプロモーションが自動で終わるようにする等の仕様が考えつきます。その場合、期限後にNovelFactoryクラスもコードを改修する必要がなくなりますね。

まとめ

以上、Factory Methodパターンについて説明しました。

本ページを作っていて思いましたが、なかなかサンプルコード程度では、メリットを押し出すのが難しいなというのが印象です。

もしも「複雑にしただけじゃん」……という印象でしたら、私の力不足と思います。

ちなみに、オブジェクトの生成系のパターンは、他にもいくつか存在します。例えば、Simple Factoryというパターン(GoFによるものでは無い)とFactory Methodを組み合わせて使うようなこともできそうです。

それら応用については、おいおい書いていこうと思います。

コメント

タイトルとURLをコピーしました