Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

PHPによるデザインパターン入門 - Abstract Factory〜関連する部品をまとめて作る工場

このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものgithubに上げてありますのでそちらもどうぞ。

GoF本における分類

 生成+オブジェクト

はじめに

 ここではAbstract Factoryパターンについて説明します。
 「abstract factory」を直訳すると「抽象的な工場」となりますね。抽象的な工場…これは一体何なのでしょうか?
 GoFパターンの1つにFactory Methodパターンがあります。Factory Methodパターンは製品であるオブジェクトを作る「工場」を用意するパターンです。ここで見ていくAbstract Factoryパターンも同様にオブジェクトを生成するパターンの1つで、関連し合うオブジェクトの集まり、つまり部品の集まりを生成します。
 ここで、「抽象的な」というところがポイントです。この工場は「抽象的」な工場で、生成される部品も「抽象的」な部品になります。
 オブジェクト指向プログラミングでは、「物を抽象化する」ということが重要なポイントとなります。つまり、具体的な物を直接扱うのではなく、「具体的な物を抽象化した物」を扱うということです。
 Abstract Factoryパターンは、具体的なクラスを明確にすることなく、関連し合うオブジェクトの集まりを生成するパターンです。

たとえば

 データベースを使ったアプリケーションを構築する場合を考えてみましょう。
 近年の開発・設計方法論では、データベースに関する処理内容とそれ以外のアプリケーション固有の処理(ビジネスロジック)とを分けて設計・実装することが主流になっています。なぜなら、接続やデータの取得、トランザクションなどデータベースだけに関する処理は、本来ビジネスロジックとは関係がないためです。
 この手法はDAO(Data Access Object)パターンと呼ばれます。DAOパターンを適用する場合、通常エンティティ(データベースの表)の単位でクラスを作成します。
 さて、データベースを使ったアプリケーションには、当然ですが何らかのデータベースが必要になります。しかし、プログラムを作成できても肝心のデータベースがないと動作しない、ひいてはテストできないといった状況になりがちです。しかし、場合によってはデータベースが用意できないまま先行して開発を行わなければならないこともあります。このような場合、データベースアクセスをすると見せかける擬似的なオブジェクト(モックオブジェクト)を使うと非常に有効です。このオブジェクトは「アクセスしているように見せかける」だけで、実際にはデータベースにアクセスしません。つまり、データベースがなくても、ビジネスロジック側を作成したりテストできます。
 しかし、モックオブジェクトを使って開発した場合でも、最終的には実際にデータベースにアクセスするクラスに切り替える必要があります。
 これはかなり大変な作業になります。しかし、ここでビジネスロジックを記述したコードを書き換えてしまっては再度テストをおこなわなければなりません。これでは何のためにモックオブジェクトを用意したのか分からなくなってしまいます。
 データベースアクセスクラスの集まりを、整合性を保ったまま簡単に切り替える…このような場面でAbstract Factoryパターンが活躍します。

Abstract Factoryパターンとは?

 Abstract Factoryパターンはオブジェクトの生成に注目したパターンで、関連するオブジェクトをその具体的なクラスを意識させないで生成することを目的としています。
 GoF本では、Abstract Factoryパターンの目的は次のように定義されています。

互いに関連したり依存し合うオブジェクト群を、その具象クラスを明確にせずに生成するためのインターフェースを提供する。

 Abstract Factoryパターンでは、部品の役割を持つクラスとその部品を作る工場の役割を持つクラスが存在します。
 ただし、その工場には関連し合う部品を生成するためのメソッドがそれぞれ用意されます。また、関連し合う部品群の種類に応じて、その工場自身も「工場を生成するための工場」によって生成されます。
 これにより、状況に応じて生成される具体的な部品群を切り替えることができます。

 まとめると、Abstract Factoryパターンでは工場から生成される部品は抽象化されており、部品の具体的な内容や生成手順をクライアントが意識しなくて済むようになります。

Abstract Factoryパターンの構造

 Abstract Factoryパターンのクラス図と構成要素は、次のようになります。


 部品であるAbstractProductクラスを生成するためのAPIを定義するクラスです。生成するのがConcreteProductクラスではなくAbstractProductクラスとして定義されているところがポイントです。

  • ConcreteFactoryクラス

 AbstractFactoryクラスのサブクラスで、定義された生成メソッドを実装するクラスです。ここには、具体的な部品であるConcreteProductクラスを返すよう実装されます。

  • AbstractProductクラス

 部品ごとのAPIを定義するクラスです。

  • ConcreteProductクラス

 AbstractProductクラスのサブクラスで、ConcreteFactoryクラスから返される具体的な部品クラスです。

  • Clientクラス

 AbstractFactoryクラスとAbstractProductクラスのAPIのみを利用してプログラミングをおこないます。

Abstract Factoryパターンのメリット

 Abstract Factoryパターンのメリットとしては、以下のものが挙げられます。

  • 具体的なクラスをクライアントから隠蔽する

 クライアントは具体的な工場や部品に直接アクセスするのではなく、抽象化された工場や部品のAPIのみを使って部品オブジェクトを生成したりアクセスしたりできます。このため、クライアントを変更することなく、具体的な工場や部品を変更することが可能です。

  • 利用する部品群の整合性を保つ

 関連し合うオブジェクトを扱う場合、その整合性を保つことが重要になります。Abstract Factoryパターンを適用することで、オブジェクト群の整合性を容易に保つことができます。

  • 部品群の単位で切り替えができる

 ConcreteFactoryクラスは関連する部品の集まりを生成します。つまり、ConcreteFactoryクラスを切り替えることで、関連する部品の集まりを容易に切り替えることができます。

Abstract Factoryパターンの適用例

 Abstract Factoryパターンの適用例を見てみましょう。
 ここでは、商品情報と注文情報を取得するアプリケーションを示しています。Abstract Factoryパターンを適用し、実データを扱う部品群とダミーデータを扱う部品群を一度に切り替えられるようにしています。また、先ほどのDAOパターンを適用して、データの取得部分を隠蔽しています。
 まずは、AbstractFactoryクラスに相当するクラスから見ていきましょう。
 DaoFactoryインターフェースは、2つの部品を生成するためのAPIが定義されています。createItemDaoメソッドとcreateOrderDaoメソッドです。特に難しいところはありませんね。

DaoFactoryインターフェース(DaoFactory.class.php
<?php
interface DaoFactory {
    public function createItemDao();
    public function createOrderDao();
}

 次は、DaoFactoryインターフェースを実装したクラスです。それぞれのcreateItemDaoメソッドとcreateOrderDaoメソッドに注目してください。DbFactoryクラスは実データ、MockFactoryクラスはダミーデータをそれぞれ扱うDAOオブジェクトを生成していることが分かります。

DbFactoryクラス(DbFactory.class.php
<?php
require_once 'DaoFactory.class.php';
require_once 'DbItemDao.class.php';
require_once 'DbOrderDao.class.php';

class DbFactory implements DaoFactory
{
    public function createItemDao()
    {
        return new DbItemDao();
    }
    public function createOrderDao()
    {
        return new DbOrderDao($this->createItemDao());
    }
}
MockFactoryクラス(MockFactory.class.php
<?php
require_once 'DaoFactory.class.php';
require_once 'MockItemDao.class.php';
require_once 'MockOrderDao.class.php';

class MockFactory implements DaoFactory
{
    public function createItemDao()
    {
        return new MockItemDao();
    }
    public function createOrderDao()
    {
        return new MockOrderDao();
    }
}

 続いて、DAOに関連するクラス群を見ていきましょう。
 ItemDaoインターフェースは商品情報、OrderDaoインターフェースは注文情報をそれぞれ取得するためのAPIを定義しています。これらのインターフェースは、AbstractProductクラスに相当します。
 このサンプルアプリケーションでは、findByIdメソッドにIDを渡すことで情報を取り出せるようにしています。

ItemDaoクラス(ItemDao.class.php
<?php
interface ItemDao {
    public function findById($item_id);
}
OrderDaoクラス(OrderDao.class.php
<?php
interface OrderDao {
    public function findById($order_id);
}

 次はConcreteProductクラスに相当するクラスです。
 先のAbstractFactoryクラスとConcreteFactoryクラスの関係と同様に、実データを扱うクラスとダミーデータを扱うクラスに分かれています。
 実データを扱うクラスはDbItemDaoクラスとDbOrderDaoクラスです。このサンプルアプリケーションでは、商品情報と注文情報をテキストファイルに保存してあり、DbItemDaoクラスとDbOrderDaoクラスがインスタンス化されるときに読み込まれ、商品・注文の各オブジェクトが生成されるようになっています。
 データファイルと商品クラス、注文クラスについては、後ほど説明します。

DbItemDaoクラス(DbItemDao.class.php
<?php
require_once 'ItemDao.class.php';
require_once 'Item.class.php';

class DbItemDao implements ItemDao
{
    private $items;
    public function __construct()
    {
        $fp = fopen('item_data.txt', 'r');

        /**
         * ヘッダ行を抜く
         */
        $dummy = fgets($fp, 4096);

        $this->items = array();
        while ($buffer = fgets($fp, 4096)) {
            $item_id = trim(substr($buffer, 0, 10));
            $item_name = trim(substr($buffer, 10));

            $item = new Item($item_id, $item_name);
            $this->items[$item->getId()] = $item;
        }

        fclose($fp);
    }

    public function findById($item_id)
    {
        if (array_key_exists($item_id, $this->items)) {
            return $this->items[$item_id];
        } else {
            return null;
        }
    }
}
DbOrderDaoクラス(DbOrderDao.class.php
<?php
require_once 'OrderDao.class.php';
require_once 'Order.class.php';

class DbOrderDao implements OrderDao
{
    private $orders;
    public function __construct(ItemDao $item_dao)
    {
        $fp = fopen('order_data.txt', 'r');

        /**
         * ヘッダ行を抜く
         */
        $dummy = fgets($fp, 4096);

        $this->orders = array();
        while ($buffer = fgets($fp, 4096)) {
            $order_id = trim(substr($buffer, 0, 10));
            $item_ids = trim(substr($buffer, 10));

            $order = new Order($order_id);
            foreach (split(',', $item_ids) as $item_id) {
                $item = $item_dao->findById($item_id);
                if (!is_null($item)) {
                    $order->addItem($item);
                }
            }
            $this->orders[$order->getId()] = $order;
        }

        fclose($fp);
    }

    public function findById($order_id)
    {
        if (array_key_exists($order_id, $this->orders)) {
            return $this->orders[$order_id];
        } else {
            return null;
        }
    }
}

 一方のダミーデータを扱うクラスは、MockItemDaoクラスとMockOrderDaoクラスとなります。これらのクラスは、決まった商品データと注文データを返します。

MockItemDaoクラス(MockItemDao.class.php
<?php
require_once 'ItemDao.class.php';
require_once 'Item.class.php';

class MockItemDao implements ItemDao
{
    public function findById($item_id)
    {
        $item = new Item('99', 'ダミー商品');
        return $item;
    }
}
MockOrderDaoクラス(MockOrderDao.class.php
<?php
require_once 'OrderDao.class.php';
require_once 'Order.class.php';

class MockOrderDao implements OrderDao
{
    public function findById($order_id)
    {
        $order = new Order('999');
        $order->addItem(new Item('99', 'ダミー商品'));
        $order->addItem(new Item('99', 'ダミー商品'));
        $order->addItem(new Item('98', 'テスト商品'));

        return $order;
    }
}

 次に、商品クラスと注文クラスを説明します。これらのクラスは、純粋に商品の情報や注文の情報を内部に保持するだけの役割を担っています。
 商品を表すItemクラスは、コンストラクタに商品情報(商品IDと商品名)を受け取り、その値にアクセスするためのメソッドが用意されているだけの単純なクラスです。

Itemクラス(Item.class.php
<?php
class Item
{
    private $id;
    private $name;
    public function __construct($id, $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
    public function getId()
    {
        return $this->id;
    }
    public function getName()
    {
        return $this->name;
    }
}

 注文を表すOrderクラスは、コンストラクタで注文IDを受け取り、注文情報に含まれる商品情報はaddItemメソッドを通じて追加します。また、注文IDや含まれている商品情報へアクセスするためのメソッドも用意されています。

Orderクラス(Order.class.php
<?php
class Order
{
    private $id;
    private $items;
    public function __construct($id)
    {
        $this->id = $id;
        $this->items = array();
    }
    public function addItem(Item $item)
    {
        $id = $item->getId();
        if (!array_key_exists($id, $this->items)) {
            $this->items[$id]['object'] = $item;
            $this->items[$id]['amount'] = 0;
        }
        $this->items[$id]['amount']++;
    }
    public function getItems()
    {
        return $this->items;
    }
    public function getId()
    {
        return $this->id;
    }
}

 ここで、実データについて見ておきましょう。
 商品情報と注文情報は、固定長データとして用意しました。フォーマットはデータファイル内の先頭行にもありますが、詳細は表を参照してください。

商品情報(item_data.txt)
商品ID    商品名
1         限定Tシャツ
2         ぬいぐるみ
3         クッキーセット
商品情報のファイルフォーマット
項目 開始位置 終了位置 備考
商品ID 1 10
商品名 11 -
注文情報(order_data.txt)
注文ID    商品ID
1         3
2         1,3
3         1,2,3
4         2
5         2,3
注文情報のファイルフォーマット
項目 開始位置 終了位置 備考
注文ID 1 10
商品ID 11 - 複数の場合、商品IDをカンマ区切りで繋げる

 ようやく最後のクライアント側コードの説明です。
 注目していただきたいのが、このコードには具体的な部品クラス、つまりConcreteProductクラスに相当するクラスが一切出てきていないということです。
 唯一登場しているのがConcreteFactoryクラスに相当するDbFactoryクラスとMockFactoryクラスです。つまり、ConcreteFactoryクラスを切り替えるだけで、部品群を綺麗に切り替えることができています。

クライアント側コード(abstract_factory_client.php
<?php
if (isset($_POST['factory'])) {
    $factory = $_POST['factory'];

    switch ($factory) {
        case 1:
            include_once 'DbFactory.class.php';
            $factory = new DbFactory();
            break;
        case 2:
            include_once 'MockFactory.class.php';
            $factory = new MockFactory();
            break;
        default:
            throw new RuntimeException('invalid factory');
    }

    $item_id = 1;
    $item_dao = $factory->createItemDao();
    $item = $item_dao->findById($item_id);
    echo 'ID=' . $item_id . 'の商品は「' . $item->getName() . '」です<br>';

    $order_id = 3;
    $order_dao = $factory->createOrderDao();
    $order = $order_dao->findById($order_id);
    echo 'ID=' . $order_id . 'の注文情報は次の通りです。';
    echo '<ul>';
    foreach ($order->getItems() as $item) {
        echo '<li>' . $item['object']->getName();
    }
    echo '</ul>';
}
?>
<hr>
<form action="" method="post">
  <div>
    DaoFactoryの種類:
    <input type="radio" name="factory" value="1">DbFactory
    <input type="radio" name="factory" value="2">MockFactory
  </div>
  <div>
    <input type="submit">
  </div>
</form>

 最後にAbstract Factoryパターンを適用したアプリケーションのクラス図は次のようになっていますので確認してみてください。


適用例の実行結果

 Abstract Factoryパターンを適用したサンプルの実行結果は、次のようになります。

サンプルの実行結果(DbFactoryを選択)


サンプルの実行結果(MockFactoryを選択)


Abstract Factoryパターンのオブジェクト指向的要素

 Abstract Factoryパターンは「ポリモーフィズム」を活用したパターンです。
 具体的な部品クラスであるConcreteProductクラスは、それぞれの部品ごとに共通のAPIを持っています。これは親クラスであるAbstractProductクラスで定義されています。つまり、クライアント側はAbstractProductクラスのAPIだけを使ってプログラミングすることで、具体的なConcreteProductクラスに依存しないコードになります。
 このことは、部品工場であるAbstractProductクラスとConcreteFactoryクラスにも言えます。この2つのクラスには親子関係がありますが、親クラスであるAbstractFactoryクラスで提供されるAPIだけを使うことで、どのConcreteFactoryクラスなのかを意識する必要がなくなり、具体的なConcreteFactoryクラスに依存しないコードになります。
 また、工場から生成される部品はAbstractProductクラスとして生成されますので、ここでも、どのConcreteProductクラスなのかを意識する必要がありません
 このため、どのConcreteFactoryクラスを使うかを切り替えるだけで、芋づるのように生成されるConcreteProductクラス群を切り替えることができます。
 オブジェクト指向プログラミングでは、具体的なクラスではなくインターフェースや抽象化されたクラスのAPIに対してプログラミングすることが重要になります

関連するパターン

  • Factory Methodパターン、Prototypeパターン

 AbstractFactoryクラスはFactory Methodパターンを使って実装される場合が多いですが、Prototypeパターンを適用して実装することも可能です。

  • Singletonパターン

 ConcreteFactoryクラスにSingletonパターンが適用される場合が多くあります。

まとめ

 ここでは関連し合う部品群を生成するAbstract Factoryパターンについて見てきました。