抽象工厂模式
是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。
假设你正在开发一款家具商店模拟器。你的代码中包括一些 类,用于表示:
你需要设法单独生成每件家具对象,这样才能确保其风格一 致。如果顾客收到的家具风格不一样,他们可不会开心。
此外, 你也不希望在添加新产品或新风格时修改已有代码。 家具供应商对于产品目录的更新非常频繁,你不会想在每次 更新时都去修改核心代码的。
解决方案
首先,抽象工厂模式建议为系列中的每件产品明确声明接口 (例如椅子、沙发或咖啡桌)。然后,确保所有产品变体都继 承这些接口。例如,所有风格的椅子都实现 椅子 接口;所 有风格的咖啡桌都实现 咖啡桌 接口,以此类推。
接下来,我们需要声明抽象工厂——包含系列中所有产品构 造方法的接口。例如 createChair 创建椅子 、 createSofa 创建沙发 和 createCoffeeTable 创建咖啡桌 。 这些方法必 须返回抽象产品类型,即我们之前抽取的那些接口: 椅子 , 沙发 和 咖啡桌 等等。
那 么 该 如 何 处 理 产 品 变 体 呢? 对 于 系 列 产 品 的 每 个 变 体, 我 们 都 将 基 于 抽象工厂 接 口 创 建 不 同 的 工 厂 类。 每 个 工 厂 类 都 只 能 返 回 特 定 类 别 的 产 品, 例 如, 现代家具工厂 ModernFurnitureFactory 只 能 创 建 现代椅子 ModernChair 、 现代沙发 ModernSofa 和 现代咖啡桌 ModernCoffeeTable 对象。
客户端代码可以通过相应的抽象接口调用工厂和产品类。你 无需修改实际客户端代码,就能更改传递给客户端的工厂类, 也能更改客户端代码接收的产品变体。
假设客户端想要工厂创建一把椅子。客户端无需了解工厂类, 也不用管工厂类创建出的椅子类型。无论是现代风格,还是 维多利亚风格的椅子,对于客户端来说没有分别,它只需调 用抽象 椅子 接口就可以了。这样一来,客户端只需知道椅 子以某种方式实现了 sitOn 坐下 方法就足够了。此外,无 论工厂返回的是何种椅子变体,它都会和由同一工厂对象创 建的沙发或咖啡桌风格一致。
最后一点说明:如果客户端仅接触抽象接口,那么谁来创建 实际的工厂对象呢?一般情况下,应用程序会在初始化阶段 创建具体工厂对象。而在此之前,应用程序必须根据配置文 件或环境设定选择工厂类别。
<?php
/**
* Abstract Factory Design Pattern
*
* Intent: Lets you produce families of related objects without specifying their
* concrete classes.
*
* Example: In this example, the Abstract Factory pattern provides an
* infrastructure for creating various types of templates for different elements
* of a web page.
*
* A web application can support different rendering engines at the same time,
* but only if its classes are independent of the concrete classes of rendering
* engines. Hence, the application's objects must communicate with template
* objects only via their abstract interfaces. Your code should not create the
* template objects directly, but delegate their creation to special factory
* objects. Finally, your code should not depend on the factory objects either
* but, instead, should work with them via the abstract factory interface.
*
* As a result, you will be able to provide the app with the factory object that
* corresponds to one of the rendering engines. All templates, created in the
* app, will be created by that factory and their type will match the type of
* the factory. If you decide to change the rendering engine, you'll be able to
* pass a new factory to the client code, without breaking any existing code.
*/
/**
* The Abstract Factory interface declares creation methods for each distinct
* product type.
*/
interface TemplateFactory
{
public function createTitleTemplate(): TitleTemplate;
public function createPageTemplate(): PageTemplate;
public function getRenderer(): TemplateRenderer;
}
/**
* Each Concrete Factory corresponds to a specific variant (or family) of
* products.
*
* This Concrete Factory creates Twig templates.
*/
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();
}
}
/**
* And this Concrete Factory creates PHPTemplate templates.
*/
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();
}
}
/**
* Each distinct product type should have a separate interface. All variants of
* the product must follow the same interface.
*
* For instance, this Abstract Product interface describes the behavior of page
* title templates.
*/
interface TitleTemplate
{
public function getTemplateString(): string;
}
/**
* This Concrete Product provides Twig page title templates.
*/
class TwigTitleTemplate implements TitleTemplate
{
public function getTemplateString(): string
{
return "<h1>{{ title }}</h1>";
}
}
/**
* And this Concrete Product provides PHPTemplate page title templates.
*/
class PHPTemplateTitleTemplate implements TitleTemplate
{
public function getTemplateString(): string
{
return "<h1><?= \$title; ?></h1>";
}
}
/**
* This is another Abstract Product type, which describes whole page templates.
*/
interface PageTemplate
{
public function getTemplateString(): string;
}
/**
* The page template uses the title sub-template, so we have to provide the way
* to set it in the sub-template object. The abstract factory will link the page
* template with a title template of the same variant.
*/
abstract class BasePageTemplate implements PageTemplate
{
protected $titleTemplate;
public function __construct(TitleTemplate $titleTemplate)
{
$this->titleTemplate = $titleTemplate;
}
}
/**
* The Twig variant of the whole page templates.
*/
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;
}
}
/**
* The PHPTemplate variant of the whole page templates.
*/
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;
}
}
/**
* The renderer is responsible for converting a template string into the actual
* HTML code. Each renderer behaves differently and expects its own type of
* template strings passed to it. Baking templates with the factory let you pass
* proper types of templates to proper renders.
*/
interface TemplateRenderer
{
public function render(string $templateString, array $arguments = []): string;
}
/**
* The renderer for Twig templates.
*/
class TwigRenderer implements TemplateRenderer
{
public function render(string $templateString, array $arguments = []): string
{
return \Twig::render($templateString, $arguments);
}
}
/**
* The renderer for PHPTemplate templates. Note that this implementation is very
* basic, if not crude. Using the `eval` function has many security
* implications, so use it with caution in real projects.
*/
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;
}
}
/**
* The client code. Note that it accepts the Abstract Factory class as the
* parameter, which allows the client to work with any concrete factory type.
*/
class Page
{
public $title;
public $content;
public function __construct($title, $content)
{
$this->title = $title;
$this->content = $content;
}
// Here's how would you use the template further in real life. Note that the
// page class does not depend on any concrete template classes.
public function render(TemplateFactory $factory): string
{
$pageTemplate = $factory->createPageTemplate();
$renderer = $factory->getRenderer();
return $renderer->render($pageTemplate->getTemplateString(), [
'title' => $this->title,
'content' => $this->content
]);
}
}
/**
* Now, in other parts of the app, the client code can accept factory objects of
* any type.
*/
$page = new Page('Sample page', 'This it the body.');
echo "Testing actual rendering with the PHPTemplate factory:\n";
echo $page->render(new PHPTemplateFactory());
// Uncomment the following if you have Twig installed.
// echo "Testing rendering with the Twig factory:\n"; echo $page->render(new
// TwigTemplateFactory());