『Head Firstデザインパターン』 Factoryパターン

  • 「new」を見たら「具象」と考える
    newは具象クラスをインスタンス化する。しかし、コードの柔軟性を保つためにインターフェースを使いたい。
  • 「new」の何が問題か
    本当の問題は「変更」であり、変更によってnewの使用箇所がどのような影響を受けるのかということ。
<?php
if ($picnic) {
    $duck = new MallardDuck();
} elseif ($hunting) {
    $duck = new DecoyDuck();
}
...

こういった実装は変更や拡張の際にコードを見直し何を追加すべきか検証する必要がある。多くの場合、このようなコードがアプリケーションの各部分に分散し、その結果、保守と更新が困難になり間違いを犯しやすくなる。

インターフェースに対してコーディングすると将来システムに起こりうる多数の変更を分離できる。また、多態性によってインターフェースを実装した任意の新しいクラスを動作させることができる。

SimpleFactoryパターン

<?php
    public function orderPizza(string $type): Pizza
    {
        // 変動しやすい要素
        if ($type === 'チーズ') {
            $pizza = new CheesePizza();
        } elseif ($type === 'ペパロニ') {
            $pizza = new PepperoniPizza();
        } elseif ($type === 'クラム') {
            $pizza = new ClamPizza();
        } elseif ($type === '野菜') {
            $pizza = new VeggiPizza();
        }

        // 変動しにくい要素
        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();
        return $pizza;
    }

ピザのオブジェクト作成をカプセル化するクラスを定義する。

<?php
class SimplePizzaFactory
{
    public function createPizza(string $type): Pizza
    {
        if ($type === 'チーズ') {
            $pizza = new CheesePizza();
        } elseif ($type === 'ペパロニ') {
            $pizza = new PepperoniPizza();
        } elseif ($type === 'クラム') {
            $pizza = new ClamPizza();
        } elseif ($type === '野菜') {
            $pizza = new VeggiPizza();
        }
        return $pizza;
    }
}

SimplePizzaFactoryはクライアントのためにピザを作成する。 スタティックで実装するとインスタンス化する必要がないスタティックファクトリにもできる。

<?php

class PizzaStore
{
    private SimplePizzaFactory $factory;

    /**
     * PizzaStore constructor.
     * @param SimplePizzaFactory $factory
     */
    public function __construct(SimplePizzaFactory $factory)
    {
        $this->factory = $factory;
    }

    public function orderPizza(string $type): Pizza
    {
        $pizza = $this->factory->createPizza($type);

        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();
        return $pizza;
    }
}

newをファクトリオブジェクトの作成メソッドに置き換えることで具象クラスのインスタンス化がなくなる。

FactoryMethodパターン

<?php

abstract class PizzaStore
{
    public function orderPizza(PizzaType $type): Pizza
    {
        $pizza = $this->createPizza($type);
        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();

        return $pizza;
    }

    abstract protected function createPizza(PizzaType $type): Pizza;
}

Pizzaのインスタンスを作成するファクトリメソッドはcreatePizza()メソッド。
抽象メソッドなのでサブクラスでオブジェクト作成の実装をする。

<?php

class NYStylePizzaStore extends PizzaStore
{
    public function createPizza(PizzaType $type): Pizza
    {
        if ($type->equals(PizzaType::CHEEZE())) {
            return new NYStyleCheezePizza();
        } elseif ($type->equals(PizzaType::VEGE())) {
            return new NYStyleVeggiePizza();
        }
    }
}

cratePizzaメソッドの振る舞いの定義はサブクラスに任せる。

<?php

$nyStore = new NYStylePizzaStore();
$chicagoStore = new ChicagoStylePizzaStore();

$pizza = $nyStore->orderPizza(PizzaType::CHEEZE());
echo "イーサンの注文は{$pizza->getName()}\n";
echo "\n";
$pizza = $chicagoStore->orderPizza(PizzaType::CHEEZE());
echo "ジョエルの注文は{$pizza->getName()}\n";
  • Factory Methodパターンの利点
    すべてのFactoryパターンは具象型のインスタンス化をカプセル化する。
    Factory Methodパターンは作成するオブジェクトをサブクラスに決定させることによってオブジェクト作成をカプセル化する。
    具象クラスをインスタンス化するコードがある場合、そこは頻繁に変化する部分である。
    すべての作成コードを一つのオブジェクトやメソッドに配置することで、コードの重複を避け、保守を一か所に集約する。

  • オブジェクトの作成は人生における現実
    ファクトリコードにはオブジェクトをインスタンス化するための具象クラスを使う必要がある。
    オブジェクトは作成する必要があり、そうしないとプログラムを作成することはできない。

FactoryMethodパターンを使用しない場合

<?php

class DependentPizzaStore
{
    public function createPizza(string $style, string $type): Pizza
    {
        if ($style === 'ニューヨーク') {
            if ($type === 'チーズ') {
                $pizza = new NYStyleCheesePizza();
            } elseif ($type === 'ペパロニ') {
                $pizza = new NYStylePepperoniPizza();
            } elseif ($type === 'クラム') {
                $pizza = new NYStyleClamPizza();
            } elseif ($type === '野菜') {
                $pizza = new NYStyleVeggiPizza();
            }
        } elseif ($style === 'シカゴ') {
            if ($type === 'チーズ') {
                $pizza = new ChicagoStyleCheesePizza();
            } elseif ($type === 'ペパロニ') {
                $pizza = new ChicagoStylePepperoniPizza();
            } elseif ($type === 'クラム') {
                $pizza = new ChicagoStyleClamPizza();
            } elseif ($type === '野菜') {
                $pizza = new ChicagoStyleVeggiPizza();
            }
        } else {
            throw new \Exception('エラー無効なピザの種類');
        }

        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();
        return $pizza;
    }
}

オブジェクトを直接インスタンス化すると、その具象クラスに依存することになる。 DependentPizzaStoreクラスはファクトリに委譲するのではなく、すべてのピザオブジェクトを直接作成している。

依存性逆転の原則

抽象に依存する。具象クラスに依存してはいけない。

この原則は、「実装に対してではなくインターフェースに対してプログラミングする」という原則に似ているが、抽象化に関してより強い声明となっている。この原則は高水準コンポーネントは低水準コンポーネントに依存するべきではなく、どちらも抽象に依存すべきであるということを示唆している。

PizzaStoreは高水準コンポーネントであり、ピザの実装は低水準コンポーネントであるが、PizzaStoreが具象ピザクラスに依存している。

原則を適用する

PizzaStoreの問題は、orderPizza()メソッドで実際に具象ピザクラスをインスタンス化しているため、すべてのピザの種類に依存していること。 orderPizzaメソッドからインスタンス化を取り除くためにFactoryMethodパターンを使う。

FactoryMethodパターンを使うと、高水準コンポーネントであるPizzaStoreと低水準コンポーネントである具象ピザクラスたちが抽象であるピザクラスに依存させることができる。上から下へ向かう依存関係図が逆転し、高水準モジュールと低水準モジュールの両方が抽象に依存するようになる。

依存性逆転の原則に従うための指針

  • 具象クラスへの参照を保持する変数を持たない
    newを使用すると、具象クラスへの参照を保持することになる。ファクトリを使ってこれを回避する。
  • 具象クラスからクラスを継承しない
    具象クラスを継承すると具象クラスに依存することになる(つまり具象クラスの変更の影響をもろに受ける)。インターフェースや抽象クラスなどの抽象を継承しましょう。
  • どのベースクラスの実装済みメソッドもオーバーライドしない
    実装済みのメソッドをオーバーライドすると、ベースクラスが開始点となるべき本当の抽象ではなくなってしまう。ベースクラスで実装済みのメソッドはすべてのサブクラスで共有することを目的としている。

すべてのプログラムをこれらの指針に従うのは不可能であり、これらはあくまでできるだけ従うように心がけるべき指針。設計の際にこれらの指針を頭にとどめておけば、この原則に違反していることを認識し、適切な理由があって違反することになる。 たとえば、ほとんど変更されないクラスはわざわざ抽象化する必要はない。

AbstractFactoryパターン

Abstract Factoryパターンは具象クラスを指定することなく、一連の関連オブジェクトや依存オブジェクトを作成するためのインターフェースを提供する。

食材を変更できるようにする

抽象ファクトリは一連の製品を作成するためのインターフェースを提供する。
PizzaIngredientFactoryインターフェースはすべての具象ファクトリが実装する必要のあるインターフェースを定義する。

<?php

interface PizzaIngredientFactory
{
    public function createDough(): Dough;

    public function createSauce(): Sauce;

    public function createCheese(): Cheese;

    public function createVeggies(): array;

    public function createPepperoni(): Pepperoni;

    public function createClam(): Clams;
}

具象ピザファクトリはそれぞれの食材のオブジェクトの作成方法を知っている。

<?php

class NYPizzaIngredientFactory implements PizzaIngredientFactory
{
    public function createDough(): Dough
    {
        return new ThinCrustDough();
    }

    public function createSauce(): Sauce
    {
        return new MarinaraSauce();
    }

    public function createCheese(): Cheese
    {
        return new ReggianoCheese();
    }

    public function createVeggies(): array
    {
        return [new Garlic(), new Onion(), new Mushroom(), new RedPepper()];
    }

    public function createPepperoni(): Pepperoni
    {
        return new SlicedPepperoni();
    }

    public function createClam(): Clams
    {
        return new FreshClams();
    }
}
<?php

class NYPizzaStore extends PizzaStore
{
    /**
     * Pizzaのインスタンスを作成するファクトリメソッド
     * 抽象メソッドなので、サブクラスでオブジェクト作成の実装をする
     * cratePizzaメソッドの振る舞いの定義はサブクラスに任せる
     * @param PizzaType $type
     * @return Pizza
     */
    protected function createPizza(PizzaType $type): Pizza
    {
        $ingredientFactory = new NYPizzaIngredientFactory();

        if ($type->equals(PizzaType::CHEEZE())) {
            $pizza = new CheesePizza($ingredientFactory);
            $pizza->setName('ニューヨークスタイルチーズピザ');
        }

        return $pizza;
    }
}
<?php

class CheesePizza extends Pizza
{
    private PizzaIngredientFactory $ingredientFactory;

    /**
     * CheesePizza constructor.
     * @param PizzaIngredientFactory $IngredientFactory
     */
    public function __construct(PizzaIngredientFactory $IngredientFactory)
    {
        $this->ingredientFactory = $IngredientFactory;
    }

    public function prepare(): void
    {
        $this->dough = $this->ingredientFactory->createDough();
        $this->sauce = $this->ingredientFactory->createSauce();
        $this->cheese = $this->ingredientFactory->createCheese();
        $this->veggies = $this->ingredientFactory->createVeggies();

        echo "{$this->name}を下処理\n";
        echo "{$this->dough->getName()}をこねる・・・\n";
        echo "{$this->sauce->getName()}を追加・・・\n";
        echo "トッピングを追加:\n";
        foreach ($this->veggies as $veggy) {
            echo "{$veggy->getName()}\n";
        }
    }
}

コンストラクタにファクトリを注入することで様々なスタイルのピザを提供する。

クライアント側のコード

<?php

$nyPizzaStore = new NYPizzaStore();
$cheesePizza = $nyPizzaStore->orderPizza(PizzaType::CHEEZE());
$cheesePizza->getName();

AbstractFactoryによってクライアントが抽象インターフェースを使って、実際に作成される具体的な製品を知ることなく関連するいち製品を作成できる。

FactoryMethodパターンとAbstractFactoryパターンの違い

  • 共通点はオブジェクトの生成が目的なこと。
  • FactoryMethodパターンは継承を利用する。オブジェクトを作成するためのファクトリメソッドを実装したサブクラスに移譲する。
  • AbstractFactoryパターンはオブジェクトコンポジションを利用する。オブジェクト生成をファクトリインターフェースで公開されたメソッドで実装する。