ITOO's Blog

プログラミングを語る

Abstract Factoryパターンについて考える

はじめに

GoFデザインパターンでAbstract Factoryパターンについて考える機会があったのでここに残そうと思います。デザインパターンを学習していてコードは理解できるのですが、実際に問題に「どう適用するか」ってところで詰まってしまうことが多いなあと思うこの頃。ゆくゆくは具体的に役立ちそうな実装を紹介したいのですが、今回は「鍋」にたとえてAbstract Factoryとその利点について重点的に書いていきます。

概要

Abstract Factoryを直訳すると「抽象的な工場」という意味、簡単に行ってしまうとオブジェクトを作る工場を作るパターンのこと。ここで「工場」というのはもちろんクラスのことです。

参考にしたページによると

AbstractFactory パターンとは、 インスタンスの生成を専門に行うクラスを用意することで、整合性を必要とされる一連のオブジェクト群を間違いなく生成するためのパターンです。

8. AbstractFactory パターン | TECHSCORE(テックスコア)

鍋に例える

正直まだわからないと思います(僕はわかりませんでした)。そこで鍋にたと例えてみたいと思います。

鍋がオブジェクトで、

f:id:arinko7478:20180917141829j:plain

具材がそのメンバ変数とします。

f:id:arinko7478:20180917141912j:plainf:id:arinko7478:20180917141916j:plain

鍋を作るのに必要なもの

  • スープ(鶏ガラスープ、キムチスープ、トマトスープ...etc)
  • メインの具材(鶏肉、牛肉、豚肉 ...etc)
  • 野菜たち(にんじん、じゃがいも、玉ねぎ ...etc)
  • そのたの具材(ご飯、麺 ...etc)

ざっと普通の鍋でしたらこんなところでしょう。

鍋クラスを作る

<?php

class HotPot 
{
    /** 鍋  */
    private $pot;

    /** スープ */
    private $soup;

    /** メインの具材 */
    private $main_guzai;

    /** 野菜 */
    private $vegetables;

    /** その他の具材 */
    private $othre_guzai;

    public function __construct($soup) 
    {
        $this->soup = $soup;
    }

    /** 以下は具材を加える関数群 */
    
    public function addSoup($soup)
    {
        $this->soup = $soup;
    }

    public function addMainGuzai($main_guzai)
    {
        $this->main_guzai = $main_guzai;
    }

    public function addVegetables($vegetables)
    {
        $this->vegetables = $vegetables;
    }

    public function addOtherGuzai($othre_guzai)
    {
        $this->othre_guzai = $othre_guzai;
    }
}

ここで「工場」は「料理人」?

料理人は鍋に合わせて具材を正しい順番で(火の通りなどを考えて)入れる必要が有る。

下記はミルフィーユ鍋をつくる 「料理人クラス」

<?php
class Cook
{
    public function prepareMillefeuille() 
    {
        // 鍋の用意
        $hot_pot = new HotPot();

        // かつおのだし汁を入れる
        $hot_pot->addSoup(new KatuoSoup());

        // 豚肉を入れる
        $hot_pot->addMainGuzai(new Butaniku());

        // 野菜たちを入れる
        $hot_pot->addVegetables([
            new Hakusai(),
            new Tamanegi()
        ]);

        // 締めのご飯を入れる
        $hot_pot->addOtherGuzai([
            new Rice()
        ]);
    }
}

「料理人」は「プログラマ」?

間違わないようにすべてのメンバ変数に値を入れてオブジェクトを作るのは難しいので、料理人がどうすれば楽になるかを考えれば自ずとプログラマが楽になっていくはず

ここで、ついにミルフィーユ鍋生成クラス(工場)を作ります。

<?php
require_once __DIR__ . '/Factory.php';

/**
 * ミルフィーユ鍋に必要な具材を持っているクラス
 */
class MillefeuilleFactory extends Factory
{
    public function getSoup()
    {
        return new KatuoSoup();
    }

    public function getMainGuzai()
    {
        return new Butaniku();
    }

    public function getVegetables()
    {
        return [
            new Hakusai(),
            new Tamanegi()
        ];
    }
    public function getOtherGuzai()
    {
        return [
            new Rice
        ];
    }
}

Factory抽象クラスについてはあとで説明します。このクラスができることで料理人はgetメソッドを呼んでオブジェクトに値を入れるだけになりました。さらに各具材の「したごしらえ」などの処理もこのクラスに書いておくことができます。

<?php
require_once __DIR__ . 'HotPot.php';
require_once __DIR__ . 'factories/MillefeuilleFactory.php';

class Cook
{
    public function prepareMillefeuille() 
    {
        // 鍋の用意
        $hot_pot = new HotPot();
        $millefeuile_factory = new MillefeuilleFactory();

        // かつおのだし汁を入れる
        $hot_pot->addSoup($millefeuile_factory->getSoup());

        // 豚肉を入れる
        $hot_pot->addMainGuzai($millefeuile_factory->getMainGuzai());

        // 野菜たちを入れる
        $hot_pot->addVegetables($millefeuile_factory->getVegetables());

        // 締めのご飯を入れる
        $hot_pot->addOtherGuzai($millefeuile_factory->getOtherGuzai());
    }
}

もっと料理人(プログラマ)を楽にする

一度でてきたFactory抽象クラスを理解してもっと楽にいろいろな鍋が作れるようにしていきたいと思います。 まずFactory抽象クラスは下のようになっています。

<?php
abstract class Factory
{
    public abstract function getSoup();
    public abstract function getMainGuzai();
    public abstract function getVegetables();
    public abstract function getOtherGuzai(); 
}

もう一度Cookクラスを見てもらいたいと思いますが、Factory抽象クラスを継承したクラスを使って鍋の準備を行うという縛りがあれば料理人のやることはどの鍋でも同じだということです。ただ違うのは何鍋の生成用インスタンスをつくるかだけです。

<?php
class Cook
{
    public function prepare($nabe)
    {
        // 鍋の用意
        $hot_pot = new HotPot();

        // 鍋のファクトリーを作成
        $factory = $this->createFactory($nabe);

        // スープを入れる
        $hot_pot->addSoup($factory->getSoup());

        // メインの具材を入れる
        $hot_pot->addMainGuzai($factory->getMainGuzai());

        // 野菜たちを入れる
        $hot_pot->addVegetables($factory->getVegetables());

        // その他の具材を入れる
        $hot_pot->addOtherGuzai($factory->getOtherGuzai());
    }

    public function createFactory($nabe)
    {
        if ($nabe === 'ミルフィーユ鍋') {
            return new MillefeuilleFactory;

        } elseif ($nabe === 'キムチ鍋') {
            return new KimutiFactory;

        } elseif ($nabe === 'すき焼き') {
            return new SukiyakiFactory;
        }
    }
}

鍋の名前によって生成するインスタンスを振り分けています。これで鍋生成クラスを増やして、それをCookに教えてやる(createFactoryメソッドを少し修正)するだけでレパートリーが増やすことができるようになりました。

まとめ

鍋に例えることでAbstract Factoryの便利さがわかったのではないでしょうか?だいぶ遠回りな説明になってしまった気もしますが、このパターンを適用することで複雑な生成過程が必要なクラスを簡単に生成することができるようになります。まず抽象クラスを用意して、それを継承したオブジェクト生成クラスを作ってやることで難しい生成処理を共通化してしまうのです。

最後に

OOPデザインパターンで有用な使い方について試行錯誤しています。なにかご指摘、要望等いただけたらうれしいです。これからもプログラミングに役立つ記事を書いていきたいと思います!ではでは( ̄ω ̄)

参考資料

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)