本記事に掲載したサンプルコードは、https://github.com/ryo-utsunomiya/design_patternでも公開中です。
Abstract Factoryとは
Abstract Factoryパターンでは、抽象的な部品を組み合わせて抽象的な製品を作ります。
部品の具体的な実装には注目せず、インタフェース(API)に注目します。このインタフェース(API)だけを使って、部品を組み立て、製品にまとめます。
ここでは、「HTMLのリンク集のHTMLを作成するファクトリ」を実装します。
まず、抽象的な製品を定義します。
<?php
namespace AbstractFactoryFramework;
abstract class Item
{
/**
* @var string
*/
protected $caption;
/**
* @param string $caption
*/
public function __construct($caption)
{
$this->caption = (string)$caption;
}
/**
* @param $str
* @return string
*/
protected static function h($str)
{
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
public abstract function makeHTML();
}
Itemクラスは、すべての部品の親となるクラスです。Itemの子クラスは、makeHTML()メソッドを実装することが期待されます。
次に、HTMLのリンクを表す抽象的な部品として、Linkを実装します。
<?php
namespace AbstractFactoryFramework;
abstract class Link extends Item
{
/**
* @var string
*/
protected $url;
/**
* @param string $caption
* @param string $url
*/
public function __construct($caption, $url)
{
parent::__construct($caption);
$this->url = (string)$url;
}
}
Linkは、captionに加えて、urlの情報を保持できるのが特徴です。
次に、Itemを格納する抽象的なTrayを作ります。
<?php
namespace AbstractFactoryFramework;
abstract class Tray extends Item
{
/**
* @var Item[]
*/
protected $tray = [];
/**
* @param string $caption
*/
public function __construct($caption)
{
parent::__construct($caption);
}
/**
* @param Item $item
*/
public function add(Item $item)
{
array_push($this->tray, $item);
}
}
Trayには、Itemを追加していくことができます。Tray自身もItemなので、TrayにTrayを格納することもできます。
次に、HTMLページ全体をまとめたPageクラスを作ります。
<?php
namespace AbstractFactoryFramework;
abstract class Page
{
/**
* @var string
*/
protected $title;
/**
* @var string
*/
protected $author;
/**
* @var Item[]
*/
protected $content = [];
/**
* @param string $title
* @param string $author
*/
public function __construct($title, $author)
{
$this->title = (string)$title;
$this->author = (string)$author;
}
/**
* @param Item $item
*/
public function add(Item $item)
{
array_push($this->content, $item);
}
public function output()
{
$filename = $this->title . ".html";
if (file_put_contents($filename, $this->makeHTML())) {
printf("Created: %sn", $filename);
} else {
printf("Failed to create: %sn", $filename);
}
}
public abstract function makeHTML();
}
Page::makeHTML()を呼ぶと、HTMLが生成されます。Page::output()で、生成したHTMLをファイルに書き込みます。
最後に、抽象的な工場であるFactoryを作ります。
<?php
namespace AbstractFactoryFramework;
abstract class Factory
{
/**
* @param string $className
* @return Factory
* @throws RuntimeException
*/
public static function getFactory($className)
{
if (class_exists($className)) {
$factory = new $className();
if ($factory instanceof Factory) {
return $factory;
} else {
throw new RuntimeException(sprintf('Class %s is not a Factory.', $className));
}
} else {
throw new RuntimeException(sprintf('Class %s Not Found', $className));
}
}
/**
* @param string $caption
* @param string $url
* @return Link
*/
public abstract function createLink($caption, $url);
/**
* @param string $caption
* @return Tray
*/
public abstract function createTray($caption);
/**
* @param string $title
* @param string $author
* @return Page
*/
public abstract function createPage($title, $author);
}
/**
* @param $str
* @return string
*/
function h($str)
{
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
Fuctory::getFactory()に具体的なファクトリークラスの名前を渡すと、そのファクトリークラスが返ります。Factory::createPage()等のメソッドに具体的なHTML生成処理を記述します。
これらのファクトリーは、以下のようなコードで使用することを想定しています。
<?php
namespace AbstractFactoryFramework;
require_once __DIR__ . '/../autoload.php';
if (!isset($argv[1])) {
printf("Usage: php abstract_factory.php \AbstractFactory\ListFactory\ListFactoryn");
exit;
}
try {
$factory = Factory::getFactory($argv[1]);
} catch (RuntimeException $e) {
echo $e->getMessage() . PHP_EOL;
exit;
}
$traySearchEngine = $factory->createTray('Search Engine');
$traySearchEngine->add($factory->createLink('Google', 'https://www.google.com/'));
$traySearchEngine->add($factory->createLink('DuckDuckGo', 'https://duckduckgo.com/'));
$page = $factory->createPage('LinkPage', 'ryo');
$page->add($traySearchEngine);
$page->output();
上記コードでは、コマンドラインから引数を受け取って、Factory::getFactory()で具体的なファクトリークラスを初期化します。
次に、検索エンジンのリンクを格納するTrayを作って、そこに検索エンジンのリンクを追加しています。
最後に、Factory::createPage()で具体的なPageを作成し、PageにTrayを追加し、Page::output()でHTMLを出力しています。
ポイントは、ここで呼び出されているメソッドはすべて抽象クラスに用意されているものだけ、ということです。
このように実装することで、具体的なファクトリーの交換が容易になります。
それでは、これらの処理に対応した具体的なクラスを実装していきましょう。
ここではListFactoryという、リンクをリスト形式に表現するファクトリーを実装します。
<?php
namespace AbstractFactoryListFactory;
use AbstractFactoryFrameworkLink;
class ListLink extends Link
{
/**
* @return string
*/
public function makeHTML()
{
return sprintf('<li><a href="%s">%s</a></li>'."n", self::h($this->url), self::h($this->caption));
}
}
ListLink::makeHTML()は、シンプルに、liタグで囲まれたaタグを返します。
<?php
namespace AbstractFactoryListFactory;
use AbstractFactoryFrameworkTray;
class ListTray extends Tray
{
/**
* @return string
*/
public function makeHTML()
{
$html = '';
$html .= "<li>n";
$html .= $this->caption . "n";
$html .= "<ul>n";
foreach ($this->tray as $item) {
$html .= $item->makeHTML();
}
$html .= "</ul>n";
$html .= "</li>n";
return $html;
}
}
ListTray::makeHTML()は、Tray内のリストをひとまとめにします。
<?php
namespace AbstractFactoryListFactory;
use AbstractFactoryFrameworkPage;
class ListPage extends Page
{
/**
* @return string
*/
public function makeHTML()
{
$html = '';
$html .= "<html><head><title>" . $this->title . "</title></head>n";
$html .= "<body>n";
$html .= "<h1>" . $this->title . "</h1>n";
$html .= "<ul>n";
foreach ($this->content as $item) {
$html .= $item->makeHTML();
}
$html .= "</ul>n";
$html .= "<hr><address>" . $this->author . "</address>n";
$html .= "</body>n";
$html .= "</html>n";
return $html;
}
}
ListPage::makeHTML()では、HTML全体を生成します。
<?php
namespace AbstractFactoryListFactory;
use AbstractFactoryFrameworkFactory;
class ListFactory extends Factory
{
/**
* @param string $caption
* @param string $url
* @return ListLink
*/
public function createLink($caption, $url)
{
return new ListLink($caption, $url);
}
/**
* @param string $caption
* @return ListTray
*/
public function createTray($caption)
{
return new ListTray($caption);
}
/**
* @param string $title
* @param string $author
* @return ListPage
*/
public function createPage($title, $author)
{
return new ListPage($title, $author);
}
}
最後に、ListFactoryです。ここでは、ListFactoryの具体的な部品を生成して返します。
ここではリストを使ったファクトリーを実装しましたが、テーブルを実装したファクトリーに差し替えることは簡単です。
このように、Abstract Factoryを使うと、様々なオブジェクトが絡んだ処理を、容易に差し替えできるようになります。