Абстрактная фабрика — это порождающий паттерн проектирования, который решает проблему создания целых семейств связанных продуктов, без указания конкретных классов продуктов.
Абстрактная фабрика задаёт интерфейс создания всех доступных типов продуктов, а каждая конкретная реализация фабрики порождает продукты одной из вариаций. Клиентский код вызывает методы фабрики для получения продуктов, вместо самостоятельного создания с помощью оператора new
. При этом фабрика сама следит за тем, чтобы создать продукт нужной вариации.
Концептуальный пример
Этот пример показывает структуру паттерна Абстрактная фабрика , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\AbstractFactory\Conceptual;
/**
* Интерфейс Абстрактной Фабрики объявляет набор методов, которые возвращают
* различные абстрактные продукты. Эти продукты называются семейством и связаны
* темой или концепцией высокого уровня. Продукты одного семейства обычно могут
* взаимодействовать между собой. Семейство продуктов может иметь несколько
* вариаций, но продукты одной вариации несовместимы с продуктами другой.
*/
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());
Output.txt: Результат выполнения
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.)
Пример из реальной жизни
В этом примере паттерн Абстрактная фабрика предоставляет инфраструктуру для создания нескольких разновидностей шаблонов для одних и тех же элементов веб-страницы.
Чтобы веб-приложение могло поддерживать сразу несколько разных движков рендеринга страниц, его классы должны работать с шаблонами только через интерфейсы, не привязываясь к конкретным классам. Чтобы этого достичь, объекты приложения не должны создавать шаблоны напрямую, а поручать создание специальным объектам-фабрикам, с которыми тоже надо работать через абстрактный интерфейс.
Благодаря этому, вы можете подать в приложение фабрику, соответствующую одному из движков рендеринга, зная, что с этого момента, все шаблоны будут порождаться именно этой фабрикой, и будут соответствовать движку рендеринга этой фабрики. Если вы захотите сменить движок рендеринга, то всё что нужно будет сделать — это подать в приложение объект фабрики другого типа и ничего при этом не сломается.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\AbstractFactory\RealWorld;
/**
* Интерфейс Абстрактной фабрики объявляет создающие методы для каждого
* определённого типа продукта.
*/
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 is 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());
Output.txt: Результат выполнения
Testing actual rendering with the PHPTemplate factory:
<div class="page">
<h1>Sample page</h1>
<article class="content">This it the body.</article>
</div>