Шаблоны проектирования в PHP : Фабрика

Фабрика (Factory) — один из наиболее часто применяемых шаблонов проектирования в программировании, обычно его используют в случае, когда во время исполнения программы необходимо выбрать один из взаимозаменяемых классов.

В целом, это удобный способ создания объектов. Фабрика (Factory) способна создавать объекты различных классов, при этом ей совсем необязательно знать тип объекта который она создает.

Шаблон проектирования Фабрика направлен на решение проблемы сильно связанных классов в больших приложениях. Она позволяет создавать объекты без вызова new.

Существует несколько способов реализации этого шаблона:

  • Простая фабрика (Simple Factory)
  • Абстрактная фабрика (Abstract Factory)
  • Фабричный метод (Factory Method)

В этой статье мы рассмотрим последний метод, так как он более близок к официальному определению шаблона и чаще находит применение в практическом программировании. Так же, хотелось бы отметить, что шаблон проектирования Простая фабрика, большинство разработчиков не считают шаблоном вовсе. Лично я считаю, что этот способ реализации вполне достоин называться шаблоном, хотя он и очень прост в реализации. Но этот спор — не цель статьи. Из названия понятно, что способ Фабричный метод использует метод класса для создания объектов.

Когда же использовать шаблон фабрика?

Теперь мы знаем, что из себя представляет такой шаблон проектирования, как фабрика, но когда же целесообразно его использовать? Вот несколько примеров:

  1. Класс не может заранее знать какого типа объект ему предстоит создать
  2. Мы хотим скрыть логику создания сложного объекта
  3. Хотим уменьшить сильные связи между классами приложения

А теперь давайте перейдём к практическим примерам использования шаблона проектирования фабрика.

А где же код?

Представьте, что мы работаем первый день на проектом Фабрика изделий Икея и нам предстоит поддерживать и обновлять существующий код. Как вы можете себе представить у Икеи есть бесконечное множество разных изделий и типов изделий. Только представьте себе, если бы каждое изделие соответствовало отдельному классу. Тогда бы наш код выглядел примерно так:

<?php
abstract class Product
{
    private   $sku;
    private   $name;
    protected $type = null;
    public function __construct($sku, $name)
    {
        $this->sku  = $sku;
        $this->name = $name;
    }
    public function getSku()
    {
        return $this->sku;
    }
    public function getName()
    {
        return $this->name;
    }
    public function getType()
    {
        return $this->type;
    }
}
class ProductChair extends Product
{
    protected $type = 'chair';
}
class ProductTable extends Product
{
    protected $type = 'table';
}
class ProductBookcase extends Product
{
    protected $type = 'bookcase';
}
class ProductSofa extends Product
{
    protected $type = 'sofa';
}

В примере выше мы видим абстрактный класс Product, реализацию конструктора объекта этого класса, и набор геттеров и сеттеров. Для каждого продукта у нас есть свой класс, который наследуются от класса Product.

Вы наверняка заметили из кода выше, что необходимо будет сделать множество вызовов для инициализации каждого объекта, например:

<?php
$product = new ProductChair('0001','INGOLF Chair');
...
$product = new ProductTable('0002','STOCKHOLN Table');
...

Проблема становится более явной если, например, у нас есть контроллер по адресу http://factory.ikea.com/product/create/{product_type}, который отвечает за создание объектов в зависимости от параметра product_type, проверку и добавление информации о объекте и его сохранение.

На время опустим логику проверки и добавления информации, для нас сейчас важнее то, как создаётся объект без использования шаблона.

<?php
class ProductController
{
    public function create($productType)
    {
        // Логика валидации продукта
        // Здесь нам нужно создать нужный продукт
        switch($productType)
        {
            case 'chair':
                $product = new ProductChair($post['sku'], $post['name']);
                break;
            case 'table':
                $product = new ProductTable($post['sku'], $post['name']);
                break;
            case 'sofa':
                $product = new ProductSofa($post['sku'], $post['name']);
                break;
            case 'bookcase':
                $product = new ProductBookcase($post['sku'], $post['name']);
                break;
        }
        // Do something with the post data and save the product
        // ...
        return $product->getType();
    }
}

Какова же основная проблема? Так как заранее мы не знаем какой тип объекта нам понадобится, нам приходится использовать switch для инициализации.

Вы наверняка спросите, что же в этом плохого. Цель ведь достигнута. Не забывайте, что это в нашем приложении сотни разных классов, то есть нам бы пришлось использовать сто выражений case.

Предыдущий пример к тому же довольно тяжело поддерживать и расширять. Что если бы нам пришлось добавить новый параметр в метод __construct? Как следствие нам пришлось бы обновить каждый вызов этого метода.

Давайте посмотрим как использование шаблона Фабричный метод может упростить нам жизнь. Для начала создадим новый класс ProductFactory.

<?php
class ProductFactory
{
    public static function build($productType,  $sku, $name)
    {
        $product = "Product" . ucfirst($productType);
        if (class_exists($product)) {
            return new $product($sku, $name);
        } else {
            throw new \Exception("Неверный тип продукта");
        }
    }
}

Теперь используем этот класс в нашем контроллере:

<?php
class ProductController
{
    public function create($productType)
    {
        // Валидация продукта
        // Здесь создаём продукт с помощью Фабричного метода
        $product = ProductFactory::build($productType, $post['sku'], $post['name']);
        // Что-то делаем с продуктом и сохраняем
        // ...
        return $product->getType();
    }
}

Только представьте, только что мы заменили сотни строк кода на одну $product = ProductFactory::build($productType, $post[‘sku’], $post[‘name’]);. Теперь если бы мы столкнулись с проблемой изменения логики инициализации в конструкторе, нам бы пришлось исправить только код в этом классе.

Выводы

В статье мы рассмотрели преимущества использования такого шаблона проектирования как Фабричный метод. Он позволяет не только упростить код, но и сделать его более простым для дальнейшей поддержки. Несмотря на то, что примеры мы использовали довольно простые, мы смогли показать суть шаблона.

Исходный код шаблона доступен на github. Оставляйте свои замечания и исправления в комментариях.