(звезда) Также известен как: Abstract Factory (1)

/**/

Суть паттерна

Абстрактная фабрика — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов. (1)

Паттерн Абстрактная фабрика

Проблема

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

Семья зависимых товаров. Сообщим, Кресло + Диванчик + Столик.

Несколько вариантов этого семейства. К примеру, товары Кресло, Диванчик и Столик представлены в трёх различных стилях: Ар-деко, Викторианском и Модерне.

Таблица соответствия семейства продуктам к их вариантам

Семейства товаров и их варианты.

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

Клиенты расстраиваются, если получают несочетающиеся товары.

А также, вы не желаете заносить перемены в имеющийся код при добавлении новых товаров либо семейcтв в программку. Продавцы нередко обновляют свои сборники, и вы бы не желали поменять уже написанный код всякий раз при получении новых моделей мебели.

Решение

Для начала паттерн Абстрактная фабрика дает выделить общие интерфейсы для некоторых товаров, которые составляют семейства. Например, все варианты кресел получат общий интерфейс Кресло, все диваны реализуют интерфейс Диванчик и т.д..

Схема иерархии классов кресел.

Все варианты 1-го и такого же объекта должны жить в одной иерархии классов.

Дальше вы создаёте абстрактную предприятие — общий интерфейс, который содержит способы сотворения всех товаров семейства (к примеру, создатьКресло, создатьДиван и создатьСтолик). Эти операции должны возвращать абстрактные типы товаров, которые были представлены интерфейсами, которые мы выделили до этого — Кресла, Диваны и Столики.

Схема иерархии классов фабрик.

Определенные предприятия соответствуют определённой варианты семейства товаров.

Как насчёт вариантов товаров? Для каждой варианты семейства товаров мы должны сделать свою свою предприятие, реализовав абстрактный интерфейс. Предприятия делают товары одной варианты. К примеру, ФабрикаМодерн будет возвращать лишь КреслаМодерн,ДиваныМодерн и СтоликиМодерн.

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

Для клиентского кода должно быть индифферентно, с какой фабрикой работать.

К примеру, клиентский код просит предприятие сделать стул. Ему не известно, какого типа была эта фабрика. Ему не известно, получит викторианский либо модерновый стул. Для него принципиально, чтоб на стуле можно было посиживать и чтоб данный стул непревзойденно смотрелся с диванчиком той же предприятия.

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

Структура

Структура классов паттерна Абстрактная фабрика

Абстрактные товары объявляют интерфейсы товаров, которые соединены вместе по смыслу, но делают различные функции.

Определенные товары — большой набор классов, которые относятся к разным абстрактным продуктам (кресло/столик), но имеют одни и те же варианты (Викторианский/Модерн).

Абстрактная фабрика заявляет способы сотворения разных абстрактных товаров (кресло/столик).

Определенные предприятия относятся любая к собственной варианты товаров (Викторианский/Модерн) и реализуют способы абстрактной предприятия, разрешая создавать все товары определённой варианты.

Невзирая на то, что определенные предприятия порождают определенные товары, сигнатуры их способов должны возвращать надлежащие абстрактные товары. Это дозволит клиентскому коду, который использует предприятие, не привязываться к определенным классам товаров. Клиент сумеет работать с хоть какими вариантами товаров через абстрактные интерфейсы.

Псевдокод

В данном примере Абстрактная фабрика создаёт кросс-платформенные элементы интерфейса и смотрит за тем, чтоб они соответствовали избранной операционной системе.

Структура классов примера паттерна Абстрактной предприятия

Пример кросс-платформенного графического интерфейса пользователя.

Кросс-платформенная программа может демонстрировать одни и те же элементы интерфейса, выглядящие чуточку по-иному в разных операционных системах. В этот программе принципиально, чтоб все создаваемые элементы постоянно соответствовали текущей операционной системе. Вы бы не желали, чтоб программа, которая была запущена на Виндовс, вдруг начала демонстрировать чекбоксы в стиле macOS.

Абстрактная фабрика заявляет перечень создающих способов, которые клиентский код может применять для получения тех либо других видов частей интерфейса. Определенные предприятия относятся к разным операционным системам и делают элементы, совместимые с этой системой.

В самом начале программа описывает, какая из фабрик соответствует текущей операционке. Потом создаёт эту предприятие и отдаёт её клиентскому коду. В предстоящем клиент будет работать лишь с этой фабрикой, чтоб исключить несопоставимость возвращаемых товаров.

Клиентский код не зависит от определенных классов фабрик и частей интерфейса. Он разговаривает с ними через абстрактные интерфейсы. с помощью данному клиент может работать с хоть какой видом фабрик и частей интерфейса.

Чтоб добавить в программку новую вариацию частей (к примеру, для поддержки Линукс), для вас не надо трогать клиентский код. Довольно сделать ещё одну предприятие, производящую эти элементы.

Код на PHP

<?php

namespace RefactoringGuruAbstractFactoryConceptual;

/**
 * Паттерн Абстрактная Фабрика
 *
 * Назначение: Предоставляет интерфейс для создания семейств связанных или
 * зависимых объектов без привязки к их конкретным классам.
 */

/**
 * Интерфейс Абстрактной Фабрики объявляет набор методов, которые возвращают
 * различные абстрактные продукты. Эти продукты называются семейством и связаны
 * темой или концепцией высокого уровня. Продукты одного семейства обычно могут
 * взаимодействовать между собой. Семейство продуктов может иметь несколько
 * вариаций, но продукты одной вариации несовместимы с продуктами другой.
 */
interface AbstractFactory
{
    public function createProductA(): AbstractProductA;

    public function createProductB(): AbstractProductB;
}

/**
 * Конкретная Фабрика производит семейство продуктов одной вариации. Фабрика
 * гарантирует совместимость полученных продуктов. Обратите внимание, что
 * сигнатуры методов Конкретной Фабрики возвращают абстрактный продукт, в то
 * время как внутри метода создается экземпляр конкретного продукта.
 */
class ConcreteFactory1 implements AbstractFactory
{
    public function createProductA(): AbstractProductA
    {
        return new ConcreteProductA1();
    }

    public function createProductB(): AbstractProductB
    {
        return new ConcreteProductB1();
    }
}

/**
 * Каждая Конкретная Фабрика имеет соответствующую вариацию продукта.
 */
class ConcreteFactory2 implements AbstractFactory
{
    public function createProductA(): AbstractProductA
    {
        return new ConcreteProductA2();
    }

    public function createProductB(): AbstractProductB
    {
        return new ConcreteProductB2();
    }
}

/**
 * Каждый отдельный продукт семейства продуктов должен иметь базовый интерфейс.
 * Все вариации продукта должны реализовывать этот интерфейс.
 */
interface AbstractProductA
{
    public function usefulFunctionA(): string;
}

/**
 * Конкретные продукты создаются соответствующими Конкретными Фабриками.
 */
class ConcreteProductA1 implements AbstractProductA
{
    public function usefulFunctionA(): string
    {
        return "The result of the product A1.";
    }
}

class ConcreteProductA2 implements AbstractProductA
{
    public function usefulFunctionA(): string
    {
        return "The result of the product A2.";
    }
}

/**
 * Базовый интерфейс другого продукта. Все продукты могут взаимодействовать друг
 * с другом, но правильное взаимодействие возможно только между продуктами одной
 * и той же конкретной вариации.
 */
interface AbstractProductB
{
    /**
     * Продукт B способен работать самостоятельно...
     */
    public function usefulFunctionB(): string;

    /**
     * ...а также взаимодействовать с Продуктами A той же вариации.
     *
     * Абстрактная Фабрика гарантирует, что все продукты, которые она создает,
     * имеют одинаковую вариацию и, следовательно, совместимы.
     */
    public function anotherUsefulFunctionB(AbstractProductA $collaborator): string;
}

/**
 * Конкретные Продукты создаются соответствующими Конкретными Фабриками.
 */
class ConcreteProductB1 implements AbstractProductB
{
    public function usefulFunctionB(): string
    {
        return "The result of the product B1.";
    }

    /**
     * Продукт B1 может корректно работать только с Продуктом A1. Тем не менее,
     * он принимает любой экземпляр Абстрактного Продукта А в качестве
     * аргумента.
     */
    public function anotherUsefulFunctionB(AbstractProductA $collaborator): string
    {
        $result = $collaborator->usefulFunctionA();

        return "The result of the B1 collaborating with the ({$result})";
    }
}

class ConcreteProductB2 implements AbstractProductB
{
    public function usefulFunctionB(): string
    {
        return "The result of the product B2.";
    }

    /**
     * Продукт B2 может корректно работать только с Продуктом A2. Тем не менее,
     * он принимает любой экземпляр Абстрактного Продукта А в качестве
     * аргумента.
     */
    public function anotherUsefulFunctionB(AbstractProductA $collaborator): string
    {
        $result = $collaborator->usefulFunctionA();

        return "The result of the B2 collaborating with the ({$result})";
    }
}

/**
 * Клиентский код работает с фабриками и продуктами только через абстрактные
 * типы: Абстрактная Фабрика и Абстрактный Продукт. Это позволяет передавать
 * любой подкласс фабрики или продукта клиентскому коду, не нарушая его.
 */
function clientCode(AbstractFactory $factory)
{
    $productA = $factory->createProductA();
    $productB = $factory->createProductB();

    echo $productB->usefulFunctionB() . "n";
    echo $productB->anotherUsefulFunctionB($productA) . "n";
}

/**
 * Клиентский код может работать с любым конкретным классом фабрики.
 */
echo "Client: Testing client code with the first factory type:n";
clientCode(new ConcreteFactory1());

echo "n";

echo "Client: Testing the same client code with the second factory type:n";
clientCode(new ConcreteFactory2());

Вывод:

Client: Testing client code with the first factory type:
The result of the product B1.
The result of the B1 collaborating with the (The result of the product A1.)

Client: Testing the same client code with the second factory type:
The result of the product B2.
The result of the B2 collaborating with the (The result of the product A2.)

(звезда) Код можно запустить тут: https://3v4l.org/UDeR3

Код на PHP (пример реальной задачи)

<?php

namespace RefactoringGuruAbstractFactoryRealWorld;

/**
 * Паттерн Абстрактная Фабрика
 *
 * Назначение: Предоставляет интерфейс для создания семейств связанных или
 * зависимых объектов, без привязки к их конкретным классам.
 *
 * Пример: В этом примере паттерн Абстрактная Фабрика предоставляет
 * инфраструктуру для создания нескольких разновидностей шаблонов для одних и
 * тех же элементов веб-страницы.
 *
 * Чтобы веб-приложение могло поддерживать сразу несколько разных движков
 * рендеринга страниц, его классы должны работать с шаблонами только через
 * интерфейсы, не привязываясь к конкретным классам. Чтобы этого достичь,
 * объекты приложения не должны создавать шаблоны напрямую, а поручать создание
 * специальным объектам-фабрикам, с которыми тоже надо работать через
 * абстрактный интерфейс.
 *
 * Благодаря этому, вы можете подать в приложение фабрику, соответствующую
 * одному из движков рендеринга, зная, что с этого момента, все шаблоны будут
 * порождаться именно этой фабрикой, и будут соответствовать движку рендеринга
 * этой фабрики. Если вы захотите сменить движок рендеринга, то всё что нужно
 * будет сделать — это подать в приложение объект фабрики другого типа и ничего
 * при этом не сломается.
 */

/**
 * Интерфейс Абстрактной фабрики объявляет создающие методы для каждого
 * определённого типа продукта.
 */
interface TemplateFactory
{
    public function createTitleTemplate(): TitleTemplate;

    public function createPageTemplate(): PageTemplate;

    public function getRenderer(): TemplateRenderer;
}

/**
 * Каждая Конкретная Фабрика соответствует определённому варианту (или
 * семейству) продуктов.
 *
 * Эта Конкретная Фабрика создает шаблоны Twig.
 */
class TwigTemplateFactory implements TemplateFactory
{
    public function createTitleTemplate(): TitleTemplate
    {
        return new TwigTitleTemplate();
    }

    public function createPageTemplate(): PageTemplate
    {
        return new TwigPageTemplate($this->createTitleTemplate());
    }

    public function getRenderer(): TemplateRenderer
    {
        return new TwigRenderer();
    }
}

/**
 * А эта Конкретная Фабрика создает шаблоны PHPTemplate.
 */
class PHPTemplateFactory implements TemplateFactory
{
    public function createTitleTemplate(): TitleTemplate
    {
        return new PHPTemplateTitleTemplate();
    }

    public function createPageTemplate(): PageTemplate
    {
        return new PHPTemplatePageTemplate($this->createTitleTemplate());
    }

    public function getRenderer(): TemplateRenderer
    {
        return new PHPTemplateRenderer();
    }
}

/**
 * Каждый отдельный тип продукта должен иметь отдельный интерфейс. Все варианты
 * продукта должны соответствовать одному интерфейсу.
 *
 * Например, этот интерфейс Абстрактного Продукта описывает поведение шаблонов
 * заголовков страниц.
 */
interface TitleTemplate
{
    public function getTemplateString(): string;
}

/**
 * Этот Конкретный Продукт предоставляет шаблоны заголовков страниц Twig.
 */
class TwigTitleTemplate implements TitleTemplate
{
    public function getTemplateString(): string
    {
        return "<h1>{{ title }}</h1>";
    }
}

/**
 * А этот Конкретный Продукт предоставляет шаблоны заголовков страниц
 * PHPTemplate.
 */
class PHPTemplateTitleTemplate implements TitleTemplate
{
    public function getTemplateString(): string
    {
        return "<h1><?= $title; ?></h1>";
    }
}

/**
 * Это еще один тип Абстрактного Продукта, который описывает шаблоны целых
 * страниц.
 */
interface PageTemplate
{
    public function getTemplateString(): string;
}

/**
 * Шаблон страниц использует под-шаблон заголовков, поэтому мы должны
 * предоставить способ установить объект для этого под-шаблона. Абстрактная
 * фабрика позаботится о том, чтобы подать сюда под-шаблон подходящего типа.
 */
abstract class BasePageTemplate implements PageTemplate
{
    protected $titleTemplate;

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

/**
 * Вариант шаблонов страниц Twig.
 */
class TwigPageTemplate extends BasePageTemplate
{
    public function getTemplateString(): string
    {
        $renderedTitle = $this->titleTemplate->getTemplateString();

        return <<<HTML
        <div class="page">
            $renderedTitle
            <article class="content">{{ content }}</article>
        </div>
        HTML;
    }
}

/**
 * Вариант шаблонов страниц PHPTemplate.
 */
class PHPTemplatePageTemplate extends BasePageTemplate
{
    public function getTemplateString(): string
    {
        $renderedTitle = $this->titleTemplate->getTemplateString();

        return <<<HTML
        <div class="page">
            $renderedTitle
            <article class="content"><?= $content; ?></article>
        </div>
        HTML;
    }
}

/**
 * Классы отрисовки отвечают за преобразовании строк шаблонов в конечный HTML
 * код. Каждый такой класс устроен по-раному и ожидает на входе шаблоны только
 * своего типа. Работа с шаблонами через фабрику позволяет вам избавиться от
 * риска подать в отрисовщик шаблон не того типа.
 */
interface TemplateRenderer
{
    public function render(string $templateString, array $arguments = []): string;
}

/**
 * Отрисовщик шаблонов Twig.
 */
class TwigRenderer implements TemplateRenderer
{
    public function render(string $templateString, array $arguments = []): string
    {
        return Twig::render($templateString, $arguments);
    }
}

/**
 * Отрисовщик шаблонов PHPTemplate. Оговорюсь, что эта реализация очень простая,
 * если не примитивная. В реальных проектах используйте `eval` с
 * осмотрительностью, т.к. неправильное использование этой функции может
 * привести к дырам безопасности.
 */
class PHPTemplateRenderer implements TemplateRenderer
{
    public function render(string $templateString, array $arguments = []): string
    {
        extract($arguments);

        ob_start();
        eval(' ?>' . $templateString . '<?php ');
        $result = ob_get_contents();
        ob_end_clean();

        return $result;
    }
}

/**
 * Клиентский код. Обратите внимание, что он принимает класс Абстрактной Фабрики
 * в качестве параметра, что позволяет клиенту работать с любым типом конкретной
 * фабрики.
 */
class Page
{

    public $title;

    public $content;

    public function __construct($title, $content)
    {
        $this->title = $title;
        $this->content = $content;
    }

    // Вот как вы бы использовали этот шаблон в дальнейшем. Обратите внимание,
    // что класс страницы не зависит ни от классов шаблонов, ни от классов
    // отрисовки.
    public function render(TemplateFactory $factory): string
    {
        $pageTemplate = $factory->createPageTemplate();

        $renderer = $factory->getRenderer();

        return $renderer->render($pageTemplate->getTemplateString(), [
            'title' => $this->title,
            'content' => $this->content
        ]);
    }
}

/**
 * Теперь в других частях приложения клиентский код может принимать фабричные
 * объекты любого типа.
 */
$page = new Page('Sample page', 'This it the body.');

echo "Testing actual rendering with the PHPTemplate factory:n";
echo $page->render(new PHPTemplateFactory());


// Можете убрать комментарии, если у вас установлен Twig.

// echo "Testing rendering with the Twig factory:n"; echo $page->render(new
// TwigTemplateFactory());

Вывод: https://3v4l.org/44JLl

Testing actual rendering with the PHPTemplate factory:
<div class="page">
    <h1>Sample page</h1>
    <article class="content">This it the body.</article>
</div>

Применимость

Когда бизнес-логика программы обязана работать с различными видами связанных вместе товаров, не завися от определенных классов товаров.

Абстрактная фабрика прячет от клиентского кода детали того, как и какие непосредственно объекты будет организованы. Однако при всем этом клиентский код может работать со всеми типами создаваемых товаров, так как их общий интерфейс был заблаговременно определён.

Когда в программе уже употребляется Фабричный способ, но очередные изменения подразумевают введение новых типов товаров.

В неплохой программе каждый класс отвечает лишь за одну вещь. Если класс имеет сильно большое количество фабричных способов, они способны затуманить его главную функцию. Потому имеет смысл вынести всю логику сотворения товаров в некоторую иерархию классов, применив абстрактную предприятие.

Шаги реализации

  1. Сделайте таблицу соотношений типов товаров к вариантам семейств товаров.

  2. Сведите все варианты товаров к общим интерфейсам.

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

  4. Сделайте классы определенных фабрик, реализовав интерфейс абстрактной предприятия. Этих классов должно быть столько же, сколько и вариантов семейств товаров.

  5. Измените код инициализации программы так, чтоб она создавала определённую предприятие и передавала её в клиентский код.

  6. Поменяйте в клиентском коде участки сотворения товаров через конструктор вызовами соответственных способов предприятия.

Достоинства и недочеты

(плюс) Гарантирует сочетаемость создаваемых товаров.

(плюс) Устраняет клиентский код от привязки к определенным классам товаров.

(плюс) Выделяет код производства товаров в одно место, упрощая поддержку кода.

(плюс) Облегчает добавление новых товаров в программку.

(плюс) Реализует принцип открытости/закрытости.

(минус) Усложняет код программы из-за введения огромного количества доп классов.

(минус) Просит наличия всех типов товаров в каждой варианты.

Отношения с иными паттернами

  • Многие архитектуры начинаются с применения Фабричного метода (более простого и расширяемого через подклассы) и эволюционируют в сторону Абстрактной фабрики, Прототипа или Строителя (более гибких, но и более сложных).

  • Строитель концентрируется на построении сложных объектов шаг за шагом. Абстрактная фабрика специализируется на создании семейств связанных продуктов. Строитель возвращает продукт только после выполнения всех шагов, а Абстрактная фабрика возвращает продукт сразу же.

  • Классы Абстрактной фабрики чаще всего реализуются с помощью Фабричного метода, хотя они могут быть построены и на основе Прототипа.

  • Абстрактная фабрика может быть использована вместо Фасада для того, чтобы скрыть платформо-зависимые классы.

  • Абстрактная фабрика может работать совместно с Мостом. Это особенно полезно, если у вас есть абстракции, которые могут работать только с некоторыми из реализаций. В этом случае фабрика будет определять типы создаваемых абстракций и реализаций.

  • Абстрактная фабрика, Строитель и Прототип могут быть реализованы при помощи Одиночки.


Источники:

(1) https://refactoring.guru/ru/design-patterns/abstract-factory

Tags

Нет комментариев

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.