什么是模式??
有经验的00开发者(以及其他的软件开发者)建立了既有通用原则又有惯用方案的指令系统来指导他们编制软件。如果以结构化形式对这些问题、解决方案和命名进行描述使其系统化,那么这些原则和习惯用法就可以称为模式。例如,下面是一个模式样例:
模式名称: 信息专家(Information Expert)
问题: 给对象分配职责的基本原则是什么?
解决方案: 给类分配一种职责,使其具有完成该职责所需要的信息。
在OO设计中,模式是对问题和解决方案的已命名描述,它可以用于新的语境。理想情况下,
模式为在变化环境中如何运用和权衡其解决方案给出建议。对于特定问题,可以应用许多模式
为对象分配职责。
简单地讲,好的模式是成对的问题/解决方案,并且具有广为人知的名称,它能用于新
的语境中,同时对新情况下的应用、权衡、实现、变化等给出了建议。
模式具有名称一重要!
软件开发是一个年轻领域。年轻领域中的原则缺乏大家广泛认可的名称,这为沟通和培训带来了困难。模式具有名称,例如信息专家和抽象工厂。对模式、设计思想或原则命名具有以下好处:
·它支持将概念条理化地组织为我们的理解和记忆。
·它便于沟通。
模式被命名并且广泛发布后(我们都同意使用这个名字),我们就可以在讨论复杂设计思想时使用简语(或简图),这可以发挥抽象的优势。看看下面两个软件开发者之间使用模式名称的讨论:
Jill:“嗨!Jack,对于这个持久性子系统,让我截图时论一下外观(Fagade)的服务。我们将对Mappers使用抽象工厂,对延迟具体化使用代理(Proxy)。”
Jack:“你刚才究竟说了什么呀!
Jill:“喂!看这儿……”
‘新模式”是一种矛盾修饰法
新模式如果描述的是新思想,则应当被认为是一种矛盾修饰法。术语“模式”的真实含义是长期重复的事物。设计模式的要点并不是要表达新的设计思想。恰恰相反,优秀模式的意图是将已有的经过验证的知识、惯用法和原则汇编起来;磨砺的越多、越悠久、使用得越广泛,这样的模式就越优秀。
因此,GRASP模式陈述的并不是新思想,它们只是将为广泛使用的基本原则命名并其汇总
起来。对于OO设计专家而言,GRASP模式(其思想而非名称)应作为其基础和熟练掌握的原则。
这是最关键的!
GOF关于设计模式的著作
Kent Beck(也因极限编程而闻名)在20世纪80年代中期首先提出了软件命名模式的思想。然而,在模式、00设计和软件设计书籍的历史上,1994年是一个重要的里程碑。极为畅销并产生巨大影响Design Patterns一书[GHJV95]就是在这一年出版的,它的作者是Gamma、Helm、Johnson和Vlissides。这本书被认为是设计模式的“圣经”,它描述了23个00设计模式,并且命名为策略(Strategy)、适配器(Adaptor)等。因此,这四个人提出的模式被称为GOF(或“四人帮”)设计模式。
然而,Design Patterns一书不是入门类书籍,读者要有一定的OO设计和编程知识,而且书
中的大部分代码是用C++编写的。
GRASP是一组模式或原则吗
GRASP定义了9个基本O0设计原则或基本设计构件。有些人会问,“难道GRASP描述的是原则而不是模式吗?”《设计模式》一书的序言给出了答案:
某人的模式是其他人的原始构造块。
本书并不注重模式的标识和描述,而更关注模式的实用价值,即模式是一种优秀的学习工具,可以用来命名、表示和记忆那些基本和经典的设计思想。
一些GRASP原则是对其他设计模式的归纳
上述适配器模式的使用可以视为某些GRASP构造模块的特化:
适配器支持防止变异,因为它通过应用了接口和多态的间接对象,改变了外部接口或第三方软件包。
问题是什么?模式过多!
Pattern Almanac 2000[Rising00]列出了大约500种设计模式,并且此后又发布了数百种模式。如此之多的模式,使求知欲望强烈的程序员都没有时间去实际编程了。
解决方案:找到根本原则
是的,对于有经验的设计者来说,详细了解和记住50种以上最重要的设计模式非常重要,但是很少有人能够学习或记住1000个模式,因此需要对这些过量的模式进行有效分类。
但是,现在有好消息了:大多数设计模式可以被视为少数几个GRASP基本原则的特化。这样除了能够有助于加速对详细设计模式的学习之外,而且发现其根本的基本主题(防止变异、多态、间接性等)更为有效,它能够帮助我们透过大量细节发现应用设计技术的本质。
示例:适配器和GRASP
图说明了我的观点,可以用GRASP原则的基本列表来分析详细的设计模式。UML的泛化关系可以用来指出概念上的连接。目前,这种思想可能过于理论化。但这是必要的,当你花费了数年应用那些大量的设计模式后,你会越来越体会到本质主题的重要性,而极为细节化的适配器或策略等任何模式都将变得次要。
适配器与某些GRASP核心原则的关系
PHP的35种设计模式
模式
三个大类。
1. 创建型
在软件工程中,创建型设计模式是处理对象创建机制的设计模式,试图以适当的方式来创建对象。对象创建的基本形式可能会带来设计问题,亦或增加了设计的复杂度。创建型设计模式通过控制这个对象的创建方式来解决此问题。
2. 结构型
在软件工程中,结构型设计模式是通过识别实体之间关系来简化设计的设计模式。
3. 行为型
在软件工程中,行为设计模式是识别对象之间的通用通信模式并实现这些模式的设计模式。 通过这样做,这些模式增加了执行此通信的灵活性。
架构模式
12、组合模式(Composite)
目的
一组对象与该对象的单个实例的处理方式一致。
示例
- form类的实例包含多个子元素,而它也像单个子元素那样响应 render() 请求,当调用 render() 方法时,它会历遍所有的子元素,调用 render() 方法
- Zend_Config: 一个配置选项树,每个选项自身就是一个 Zend_Config 对象
UML
代码
- RenderableInterface.php
<?php
namespace DesignPatterns\Structural\Composite;
interface RenderableInterface
{
public function render(): string;
}
Form.php
<?php
namespace DesignPatterns\Structural\Composite;
/**
* 该组合内的节点必须派生于该组件契约。为了构建成一个组件树,
* 此为强制性操作。
*/
class Form implements RenderableInterface
{
/**
* @var RenderableInterface[]
*/
private $elements;
/**
* 遍历所有元素,并对他们调用 render() 方法,然后返回表单的完整
* 的解析表达。
*
* 从外部上看,我们不会看到遍历过程,该表单的操作过程与单一对
* 象实例一样
*
* @return string
*/
public function render(): string
{
$formCode = '<form>';
foreach ($this->elements as $element) {
$formCode .= $element->render();
}
$formCode .= '</form>';
return $formCode;
}
/**
* @param RenderableInterface $element
*/
public function addElement(RenderableInterface $element)
{
$this->elements[] = $element;
}
}
InputElement.php
<?php
namespace DesignPatterns\Structural\Composite;
class InputElement implements RenderableInterface
{
public function render(): string
{
return '<input type="text" />';
}
}
TextElement.php
<?php
namespace DesignPatterns\Structural\Composite;
class TextElement implements RenderableInterface
{
/**
* @var string
*/
private $text;
public function __construct(string $text)
{
$this->text = $text;
}
public function render(): string
{
return $this->text;
}
}
测试
Tests/CompositeTest.php
<?php
namespace DesignPatterns\Structural\Composite\Tests;
use DesignPatterns\Structural\Composite;
use PHPUnit\Framework\TestCase;
class CompositeTest extends TestCase
{
public function testRender()
{
$form = new Composite\Form();
$form->addElement(new Composite\TextElement('Email:'));
$form->addElement(new Composite\InputElement());
$embed = new Composite\Form();
$embed->addElement(new Composite\TextElement('Password:'));
$embed->addElement(new Composite\InputElement());
$form->addElement($embed);
// 此代码仅作示例。在实际场景中,现在的网页浏览器根本不支持
// 多表单嵌套,牢记该点非常重要
$this->assertEquals(
'<form>Email:<input type="text" /><form>Password:<input type="text" /></form></form>',
$form->render()
);
}
}
13、数据映射模式(Data Mapper)
目标
数据映射器是一种数据访问层,用于将数据在持久性数据存储(通常是一个关系数据库)和内存中的数据表示(领域层)之间进行双向传输。该模式的目标是为了将数据的内存表示、持久存储、数据访问进行分离。 该层由一个或多个映射器(或数据访问对象)组成,并且进行数据的转换。映射器实现的范围有所不同。 通用映射器将处理许多不同的域实体类型,专用映射器将处理一个或几个。
例子
数据库对象关系映射器( ORM ):Doctrine2 使用的 DAO,名字叫做 “EntityRepository”。
UML
代码
- User.php
<?php
namespace DesignPatterns\Structural\DataMapper;
class User
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $email;
public static function fromState(array $state): User
{
// 在你访问的时候验证状态
return new self(
$state['username'],
$state['email']
);
}
public function __construct(string $username, string $email)
{
// 先验证参数再设置他们
$this->username = $username;
$this->email = $email;
}
/**
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
}
UserMapper.php
<?php
namespace DesignPatterns\Structural\DataMapper;
class UserMapper
{
/**
* @var StorageAdapter
*/
private $adapter;
/**
* @param StorageAdapter $storage
*/
public function __construct(StorageAdapter $storage)
{
$this->adapter = $storage;
}
/**
* 根据 id 从存储器中找到用户,并返回一个用户对象
* 在内存中,通常这种逻辑将使用 Repository 模式来实现
* 然而,重要的部分是在下面的 mapRowToUser() 中,它将从中创建一个业务对象
* 从存储中获取的数据
*
* @param int $id
*
* @return User
*/
public function findById(int $id): User
{
$result = $this->adapter->find($id);
if ($result === null) {
throw new \InvalidArgumentException("User #$id not found");
}
return $this->mapRowToUser($result);
}
private function mapRowToUser(array $row): User
{
return User::fromState($row);
}
}
StorageAdapter.php
<?php
namespace DesignPatterns\Structural\DataMapper;
class StorageAdapter
{
/**
* @var array
*/
private $data = [];
public function __construct(array $data)
{
$this->data = $data;
}
/**
* @param int $id
*
* @return array|null
*/
public function find(int $id)
{
if (isset($this->data[$id])) {
return $this->data[$id];
}
return null;
}
}
测试
- Tests/DataMapperTest.php
<?php
namespace DesignPatterns\Structural\DataMapper\Tests;
use DesignPatterns\Structural\DataMapper\StorageAdapter;
use DesignPatterns\Structural\DataMapper\User;
use DesignPatterns\Structural\DataMapper\UserMapper;
use PHPUnit\Framework\TestCase;
class DataMapperTest extends TestCase
{
public function testCanMapUserFromStorage()
{
$storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => 'liebler.dominik@gmail.com']]);
$mapper = new UserMapper($storage);
$user = $mapper->findById(1);
$this->assertInstanceOf(User::class, $user);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testWillNotMapInvalidData()
{
$storage = new StorageAdapter([]);
$mapper = new UserMapper($storage);
$mapper->findById(1);
}
}
14、装饰模式(Decorator)
目的
动态地为类的实例添加功能
例子
- Zend Framework: Zend_Form_Element 实例的装饰者
- Web Service层:REST服务的JSON与XML装饰器(当然,在此只能使用其中的一种)
UML
代码
- RenderableInterface.php
<?php
namespace DesignPatterns\Structural\Decorator;
/**
* 创建渲染接口。
* 这里的装饰方法 renderData() 返回的是字符串格式数据。
*/
interface RenderableInterface
{
public function renderData(): string;
}
Webservice.php
<?php
namespace DesignPatterns\Structural\Decorator;
/**
* 创建 Webservice 服务类实现 RenderableInterface。
* 该类将在后面为装饰者实现数据的输入。
*/
class Webservice implements RenderableInterface
{
/**
* @var string
*/
private $data;
/**
* 传入字符串格式数据。
*/
public function __construct(string $data)
{
$this->data = $data;
}
/**
* 实现 RenderableInterface 渲染接口中的 renderData() 方法。
* 返回传入的数据。
*/
public function renderData(): string
{
return $this->data;
}
}
RendererDecorator.php
<?php
namespace DesignPatterns\Structural\Decorator;
/**
* 装饰者必须实现渲染接口类 RenderableInterface 契约,这是该设计
* 模式的关键点。否则,这将不是一个装饰者而只是一个自欺欺人的包
* 装。
*
* 创建抽象类 RendererDecorator (渲染器装饰者)实现渲染接口。
*/
abstract class RendererDecorator implements RenderableInterface
{
/**
* @var RenderableInterface
* 定义渲染接口变量。
*/
protected $wrapped;
/**
* @param RenderableInterface $renderer
* 传入渲染接口类对象 $renderer。
*/
public function __construct(RenderableInterface $renderer)
{
$this->wrapped = $renderer;
}
}
XmlRenderer.php
<?php
namespace DesignPatterns\Structural\Decorator;
/**
* 创建 Xml 修饰者并继承抽象类 RendererDecorator 。
*/
class XmlRenderer extends RendererDecorator
{
/**
* 对传入的渲染接口对象进行处理,生成 DOM 数据文件。
*/
public function renderData(): string
{
$doc = new \DOMDocument();
$data = $this->wrapped->renderData();
$doc->appendChild($doc->createElement('content', $data));
return $doc->saveXML();
}
}
JsonRenderer.php
<?php
namespace DesignPatterns\Structural\Decorator;
/**
* 创建 Json 修饰者并继承抽象类 RendererDecorator 。
*/
class JsonRenderer extends RendererDecorator
{
/**
* 对传入的渲染接口对象进行处理,生成 JSON 数据。
*/
public function renderData(): string
{
return json_encode($this->wrapped->renderData());
}
}
测试
- Tests/DecoratorTest.php
<?php
namespace DesignPatterns\Structural\Decorator\Tests;
use DesignPatterns\Structural\Decorator;
use PHPUnit\Framework\TestCase;
/**
* 创建自动化测试单元 DecoratorTest 。
*/
class DecoratorTest extends TestCase
{
/**
* @var Decorator\Webservice
*/
private $service;
/**
* 传入字符串 'foobar' 。
*/
protected function setUp()
{
$this->service = new Decorator\Webservice('foobar');
}
/**
* 测试 JSON 装饰者。
* 这里的 assertEquals 是为了判断返回的结果是否符合预期。
*/
public function testJsonDecorator()
{
$service = new Decorator\JsonRenderer($this->service);
$this->assertEquals('"foobar"', $service->renderData());
}
/**
* 测试 Xml 装饰者。
*/
public function testXmlDecorator()
{
$service = new Decorator\XmlRenderer($this->service);
$this->assertXmlStringEqualsXmlString('<?xml version="1.0"?><content>foobar</content>', $service->renderData());
}
}
15、依赖注入模式(Dependency Injection)
目的
用松散耦合的方式来更好的实现可测试、可维护和可扩展的代码。
依赖注入模式:依赖注入(Dependency Injection)是控制反转(Inversion of Control)的一种实现方式。要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由 IoC 容器来完成,然后在调用者中注入被调用者(通过构造器 / 方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。
用法
DatabaseConfiguration 被注入 DatabaseConnection 并获取所需的 $config 。如果没有依赖注入模式, 配置将直接创建 DatabaseConnection 。这对测试和扩展来说很不好。
例子
Doctrine2 ORM 使用依赖注入。 例如,注入到 Connection 对象的配置。 对于测试而言, 可以轻松的创建可扩展的模拟数据并注入到 Connection 对象中。
Symfony 和 Zend Framework 2 已经有了依赖注入的容器。他们通过配置的数组来创建对象,并在需要的地方注入 (在控制器中)。
UML
代码
DatabaseConfiguration.php
<?php
namespace DesignPatterns\Structural\DependencyInjection;
class DatabaseConfiguration
{
/**
* @var string
*/
private $host;
/**
* @var int
*/
private $port;
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $password;
public function __construct(string $host, int $port, string $username, string $password)
{
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->password = $password;
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): int
{
return $this->port;
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{
return $this->password;
}
}
DatabaseConnection.php
<?php
namespace DesignPatterns\Structural\DependencyInjection;
class DatabaseConnection
{
/**
* @var DatabaseConfiguration
*/
private $configuration;
/**
* @param DatabaseConfiguration $config
*/
public function __construct(DatabaseConfiguration $config)
{
$this->configuration = $config;
}
public function getDsn(): string
{
// 这仅仅是演示,而不是一个真正的 DSN
// 注意,这里只使用了注入的配置。 所以,
// 这里是关键的分离关注点。
return sprintf(
'%s:%s@%s:%d',
$this->configuration->getUsername(),
$this->configuration->getPassword(),
$this->configuration->getHost(),
$this->configuration->getPort()
);
}
}
测试
Tests/DependencyInjectionTest.php
<?php
namespace DesignPatterns\Structural\DependencyInjection\Tests;
use DesignPatterns\Structural\DependencyInjection\DatabaseConfiguration;
use DesignPatterns\Structural\DependencyInjection\DatabaseConnection;
use PHPUnit\Framework\TestCase;
class DependencyInjectionTest extends TestCase
{
public function testDependencyInjection()
{
$config = new DatabaseConfiguration('localhost', 3306, 'domnikl', '1234');
$connection = new DatabaseConnection($config);
$this->assertEquals('domnikl:1234@localhost:3306', $connection->getDsn());
}
}
16、门面模式(Facade)
目的
门面模式的最初目的并不是为了避免让你阅读复杂的 API 文档,这只是一个附带作用。其实它的本意是为了降低耦合性并且遵循 Demeter 定律。
Facade通过嵌入多个(当然,有时只有一个)接口来解耦访客与子系统,同时也为了降低复杂度。
- Facade 不会禁止你访问子系统
- 你可以(应该)为一个子系统提供多个 Facade
因此一个好的 Facade 里面不会有 new
。如果每个方法里都要构造多个对象,那么它就不是 Facade,而是生成器或者[抽象|静态|简单] 工厂 [方法]。
优秀的 Facade 不会有 new
,并且构造函数参数是接口类型的。如果你需要创建一个新实例,则在参数中传入一个工厂对象。
UML
代码
- Facade.php
<?php
namespace DesignPatterns\Structural\Facade;
class Facade
{
/**
* @var OsInterface
* 定义操作系统接口变量。
*/
private $os;
/**
* @var BiosInterface
* 定义基础输入输出系统接口变量。
*/
private $bios;
/**
* @param BiosInterface $bios
* @param OsInterface $os
* 传入基础输入输出系统接口对象 $bios 。
* 传入操作系统接口对象 $os 。
*/
public function __construct(BiosInterface $bios, OsInterface $os)
{
$this->bios = $bios;
$this->os = $os;
}
/**
* 构建基础输入输出系统执行启动方法。
*/
public function turnOn()
{
$this->bios->execute();
$this->bios->waitForKeyPress();
$this->bios->launch($this->os);
}
/**
* 构建系统关闭方法。
*/
public function turnOff()
{
$this->os->halt();
$this->bios->powerDown();
}
}
OsInterface.php
<?php
namespace DesignPatterns\Structural\Facade;
/**
* 创建操作系统接口类 OsInterface 。
*/
interface OsInterface
{
/**
* 声明关机方法。
*/
public function halt();
/**
* 声明获取名称方法,返回字符串格式数据。
*/
public function getName(): string;
}
BiosInterface.php
<?php
namespace DesignPatterns\Structural\Facade;
/**
* 创建基础输入输出系统接口类 BiosInterface 。
*/
interface BiosInterface
{
/**
* 声明执行方法。
*/
public function execute();
/**
* 声明等待密码输入方法
*/
public function waitForKeyPress();
/**
* 声明登录方法。
*/
public function launch(OsInterface $os);
/**
* 声明关机方法。
*/
public function powerDown();
}
测试
- Tests/FacadeTest.php
<?php
namespace DesignPatterns\Structural\Facade\Tests;
use DesignPatterns\Structural\Facade\Facade;
use DesignPatterns\Structural\Facade\OsInterface;
use PHPUnit\Framework\TestCase;
/**
* 创建自动化测试单元 FacadeTest 。
*/
class FacadeTest extends TestCase
{
public function testComputerOn()
{
/** @var OsInterface|\PHPUnit_Framework_MockObject_MockObject $os */
$os = $this->createMock('DesignPatterns\Structural\Facade\OsInterface');
$os->method('getName')
->will($this->returnValue('Linux'));
$bios = $this->getMockBuilder('DesignPatterns\Structural\Facade\BiosInterface')
->setMethods(['launch', 'execute', 'waitForKeyPress'])
->disableAutoload()
->getMock();
$bios->expects($this->once())
->method('launch')
->with($os);
$facade = new Facade($bios, $os);
// 门面接口很简单。
$facade->turnOn();
// 但你也可以访问底层组件。
$this->assertEquals('Linux', $os->getName());
}
}
17、流接口模式(Fluent Interface)
目的
用来编写易于阅读的代码,就像自然语言一样(如英语)
例子
- Yii 框架:CDbCommand 与 CActiveRecord 也使用此模式
- Doctrine2 的 QueryBuilder,就像下面例子中类似
- PHPUnit 使用连贯接口来创建 mock 对象
UML
代码
- Sql.php
<?php
namespace DesignPatterns\Structural\FluentInterface;
class Sql
{
/**
* @var array
*/
private $fields = [];
/**
* @var array
*/
private $from = [];
/**
* @var array
*/
private $where = [];
public function select(array $fields): Sql
{
$this->fields = $fields;
return $this;
}
public function from(string $table, string $alias): Sql
{
$this->from[] = $table.' AS '.$alias;
return $this;
}
public function where(string $condition): Sql
{
$this->where[] = $condition;
return $this;
}
public function __toString(): string
{
return sprintf(
'SELECT %s FROM %s WHERE %s',
join(', ', $this->fields),
join(', ', $this->from),
join(' AND ', $this->where)
);
}
}
测试
- Tests/FluentInterfaceTest.php
<?php
namespace DesignPatterns\Structural\FluentInterface\Tests;
use DesignPatterns\Structural\FluentInterface\Sql;
use PHPUnit\Framework\TestCase;
class FluentInterfaceTest extends TestCase
{
public function testBuildSQL()
{
$query = (new Sql())
->select(['foo', 'bar'])
->from('foobar', 'f')
->where('f.bar = ?');
$this->assertEquals('SELECT foo, bar FROM foobar AS f WHERE f.bar = ?', (string) $query);
}
}
18、享元模式(Flyweight)
目的
为了节约内存的使用,享元模式会尽量使类似的对象共享内存。在大量类似对象被使用的情况中这是十分必要的。常用做法是在外部数据结构中保存类似对象的状态,并在需要时将他们传递给享元对象。
UML
代码
- FlyweightInterface.php
<?php
namespace DesignPatterns\Structural\Flyweight;
/**
* 创建享元接口 FlyweightInterface 。
*/
interface FlyweightInterface
{
/**
* 创建传递函数。
* 返回字符串格式数据。
*/
public function render(string $extrinsicState): string;
}
CharacterFlyweight.php
<?php
namespace DesignPatterns\Structural\Flyweight;
/**
* 假如可以的话,实现享元接口并增加内存存储内部状态。
* 具体的享元实例被工厂类的方法共享。
*/
class CharacterFlyweight implements FlyweightInterface
{
/**
* 任何具体的享元对象存储的状态必须独立于其运行环境。
* 享元对象呈现的特点,往往就是对应的编码的特点。
*
* @var string
*/
private $name;
/**
* 输入一个字符串对象 $name。
*/
public function __construct(string $name)
{
$this->name = $name;
}
/**
* 实现 FlyweightInterface 中的传递方法 render() 。
*/
public function render(string $font): string
{
// 享元对象需要客户端提供环境依赖信息来自我定制。
// 外在状态经常包含享元对象呈现的特点,例如字符。
return sprintf('Character %s with font %s', $this->name, $font);
}
}
FlyweightFactory.php
<?php
namespace DesignPatterns\Structural\Flyweight;
/**
* 工厂类会管理分享享元类,客户端不应该直接将他们实例化。
* 但可以让工厂类负责返回现有的对象或创建新的对象。
*/
class FlyweightFactory implements \Countable
{
/**
* @var CharacterFlyweight[]
* 定义享元特征数组。
* 用于存储不同的享元特征。
*/
private $pool = [];
/**
* 输入字符串格式数据 $name。
* 返回 CharacterFlyweight 对象。
*/
public function get(string $name): CharacterFlyweight
{
if (!isset($this->pool[$name])) {
$this->pool[$name] = new CharacterFlyweight($name);
}
return $this->pool[$name];
}
/**
* 返回享元特征个数。
*/
public function count(): int
{
return count($this->pool);
}
}
测试
- Tests/FlyweightTest.php
<?php
namespace DesignPatterns\Structural\Flyweight\Tests;
use DesignPatterns\Structural\Flyweight\FlyweightFactory;
use PHPUnit\Framework\TestCase;
/**
* 创建自动化测试单元 FlyweightTest 。
*/
class FlyweightTest extends TestCase
{
private $characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
private $fonts = ['Arial', 'Times New Roman', 'Verdana', 'Helvetica'];
public function testFlyweight()
{
$factory = new FlyweightFactory();
foreach ($this->characters as $char) {
foreach ($this->fonts as $font) {
$flyweight = $factory->get($char);
$rendered = $flyweight->render($font);
$this->assertEquals(sprintf('Character %s with font %s', $char, $font), $rendered);
}
}
// 享元模式会保证实例被分享。
// 相比拥有成百上千的私有对象,
// 必须要有一个实例代表所有被重复使用来显示不同单词的字符。
$this->assertCount(count($this->characters), $factory);
}
}
19、代理模式(Proxy)
目的
为昂贵或者无法复制的资源提供接口。
代理模式(Proxy)为其他对象提供一种代理以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制目标对象的访问(目标对象可以是远程的对象、创建开销大的对象或需要安全控制的对象),并且可以在不改变目标对象的情况下添加一些额外的功能。
在某些情况下,一个客户不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。
经典例子就是网络代理,你想访问 Facebook 或者 Twitter ,如何绕过 GFW?找个代理网站。
例子
- Doctrine2 使用代理来实现框架特性(如延迟初始化),同时用户还是使用自己的实体类并且不会使用或者接触到代理
UML
代码
- Record.php
<?php
namespace DesignPatterns\Structural\Proxy;
/**
* @property 用户名
*/
class Record
{
/**
* @var string[]
*/
private $data;
/**
* @param string[] $data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* @param string $name
* @param string $value
*/
public function __set(string $name, string $value)
{
$this->data[$name] = $value;
}
public function __get(string $name): string
{
if (!isset($this->data[$name])) {
throw new \OutOfRangeException('Invalid name given');
}
return $this->data[$name];
}
}
RecordProxy.php
<?php
namespace DesignPatterns\Structural\Proxy;
class RecordProxy extends Record
{
/**
* @var bool
*/
private $isDirty = false;
/**
* @var bool
*/
private $isInitialized = false;
/**
* @param array $data
*/
public function __construct(array $data)
{
parent::__construct($data);
// 当记录有数据的时候,将 initialized 标记为 true ,
// 因为记录将保存我们的业务逻辑,我们不希望在 Record 类里面实现这个行为
// 而是在继承了 Record 的代理类中去实现。
if (count($data) > 0) {
$this->isInitialized = true;
$this->isDirty = true;
}
}
/**
* @param string $name
* @param string $value
*/
public function __set(string $name, string $value)
{
$this->isDirty = true;
parent::__set($name, $value);
}
public function isDirty(): bool
{
return $this->isDirty;
}
}
测试
<?php
namespace DesignPatterns\Structural\Proxy\Tests;
use DesignPatterns\Structural\Proxy\Record;
use DesignPatterns\Structural\Proxy\RecordProxy;
class ProxyTest extends \PHPUnit_Framework_TestCase
{
public function testSetAttribute(){
$data = [];
$proxy = new RecordProxy($data);
$proxy->xyz = false;
$this->assertTrue($proxy->xyz===false);
}
}
20、注册模式(Registry)
目的
目的是能够存储在应用程序中经常使用的对象实例,通常会使用只有静态方法的抽象类来实现(或使用单例模式)。需要注意的是这里可能会引入全局的状态,我们需要使用依赖注入来避免它。
例子
- Zend 框架 1:Zend_Registry 实现了整个应用程序的 logger 对象和前端控制器等
- Yii 框架:CWebApplication 具有全部应用程序组件,例如 CWebUser,CUrlManager 等。
UML
代码
- Registry.php
<?php
namespace DesignPatterns\Structural\Registry;
/**
* 创建注册表抽象类。
*/
abstract class Registry
{
const LOGGER = 'logger';
/**
* 这里将在你的应用中引入全局状态,但是不可以被模拟测试。
* 因此被视作一种反抗模式!使用依赖注入进行替换!
*
* @var array
* 定义存储值数组。
*/
private static $storedValues = [];
/**
* @var array
* 定义合法键名数组。
* 可在此定义用户名唯一性。
*/
private static $allowedKeys = [
self::LOGGER,
];
/**
* @param string $key
* @param mixed $value
*
* @return void
* 设置键值,并保存进 $storedValues 。
* 可视作设置密码。
*/
public static function set(string $key, $value)
{
if (!in_array($key, self::$allowedKeys)) {
throw new \InvalidArgumentException('Invalid key given');
}
self::$storedValues[$key] = $value;
}
/**
* @param string $key
*
* @return mixed
* 定义获取方法,获取已存储的对应键的值
* 可视作验证用户环节,检查用户名是否存在,返回密码,后续验证密码正确性。
*/
public static function get(string $key)
{
if (!in_array($key, self::$allowedKeys) || !isset(self::$storedValues[$key])) {
throw new \InvalidArgumentException('Invalid key given');
}
return self::$storedValues[$key];
}
}
测试
- Tests/RegistryTest.php
<?php
namespace DesignPatterns\Structural\Registry\Tests;
use DesignPatterns\Structural\Registry\Registry;
use stdClass;
use PHPUnit\Framework\TestCase;
/**
* 创建自动化测试单元。
*/
class RegistryTest extends TestCase
{
public function testSetAndGetLogger()
{
$key = Registry::LOGGER;
$logger = new stdClass();
Registry::set($key, $logger);
$storedLogger = Registry::get($key);
$this->assertSame($logger, $storedLogger);
$this->assertInstanceOf(stdClass::class, $storedLogger);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testThrowsExceptionWhenTryingToSetInvalidKey()
{
Registry::set('foobar', new stdClass());
}
/**
* 注 @在此运行隔离进程:没有它的话,前一个测试单元可能已经设置它,
* 并且测试将不能运行,这就是为什么你应该实现依赖注入,
* 因为注入类会很容易被测试单元替代。
*
* @runInSeparateProcess
* @expectedException \InvalidArgumentException
*/
public function testThrowsExceptionWhenTryingToGetNotSetKey()
{
Registry::get(Registry::LOGGER);
}
}
行为模式
21、责任链模式(Chain Of Responsibilities)
目的
建立一个对象链来按指定顺序处理调用。如果其中一个对象无法处理命令,它会委托这个调用给它的下一个对象来进行处理,以此类推。
例子
- 垃圾邮件过滤器。
- 日志框架,每个链元素自主决定如何处理日志消息。
- 缓存:例如第一个对象是一个 Memcached 接口实例,如果 “丢失” 它会委托数据库接口处理这个调用。
- Yii 框架: CFilterChain 是一个控制器行为过滤器链。执行点会有链上的过滤器逐个传递,并且只有当所有的过滤器验证通过,这个行为最后才会被调用。
UML
代码
- Handler.php
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* 创建处理器抽象类 Handler 。
*/
abstract class Handler
{
/**
* @var Handler|null
* 定义继承处理器
*/
private $successor = null;
/**
* 输入集成处理器对象。
*/
public function __construct(Handler $handler = null)
{
$this->successor = $handler;
}
/**
* 通过使用模板方法模式这种方法可以确保每个子类不会忽略调用继
* 承。
*
* @param RequestInterface $request
* 定义处理请求方法。
*
* @return string|null
*/
final public function handle(RequestInterface $request)
{
$processed = $this->processing($request);
if ($processed === null) {
// 请求尚未被目前的处理器处理 => 传递到下一个处理器。
if ($this->successor !== null) {
$processed = $this->successor->handle($request);
}
}
return $processed;
}
/**
* 声明处理方法。
*/
abstract protected function processing(RequestInterface $request);
}
Responsible/FastStorage.php
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
/**
* 创建 http 缓存处理类。
*/
class HttpInMemoryCacheHandler extends Handler
{
/**
* @var array
*/
private $data;
/**
* @param array $data
* 传入数据数组参数。
* @param Handler|null $successor
* 传入处理器类对象 $successor 。
*/
public function __construct(array $data, Handler $successor = null)
{
parent::__construct($successor);
$this->data = $data;
}
/**
* @param RequestInterface $request
* 传入请求类对象参数 $request 。
* @return string|null
*
* 返回缓存中对应路径存储的数据。
*/
protected function processing(RequestInterface $request)
{
$key = sprintf(
'%s?%s',
$request->getUri()->getPath(),
$request->getUri()->getQuery()
);
if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
return $this->data[$key];
}
return null;
}
}
Responsible/SlowStorage.php
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
/**
* 创建数据库处理器。
*/
class SlowDatabaseHandler extends Handler
{
/**
* @param RequestInterface $request
* 传入请求类对象 $request 。
*
* @return string|null
* 定义处理方法,下面应该是个数据库查询动作,但是简单化模拟,直接返回一个 'Hello World' 字符串作查询结果。
*/
protected function processing(RequestInterface $request)
{
// 这是一个模拟输出, 在生产代码中你应该调用一个缓慢的 (相对于内存来说) 数据库查询结果。
return 'Hello World!';
}
}
测试
- Tests/ChainTest.php
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler;
use PHPUnit\Framework\TestCase;
/**
* 创建一个自动化测试单元 ChainTest 。
*/
class ChainTest extends TestCase
{
/**
* @var Handler
*/
private $chain;
/**
* 模拟设置缓存处理器的缓存数据。
*/
protected function setUp()
{
$this->chain = new HttpInMemoryCacheHandler(
['/foo/bar?index=1' => 'Hello In Memory!'],
new SlowDatabaseHandler()
);
}
/**
* 模拟从缓存中拉取数据。
*/
public function testCanRequestKeyInFastStorage()
{
$uri = $this->createMock('Psr\Http\Message\UriInterface');
$uri->method('getPath')->willReturn('/foo/bar');
$uri->method('getQuery')->willReturn('index=1');
$request = $this->createMock('Psr\Http\Message\RequestInterface');
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
$this->assertEquals('Hello In Memory!', $this->chain->handle($request));
}
/**
* 模拟从数据库中拉取数据。
*/
public function testCanRequestKeyInSlowStorage()
{
$uri = $this->createMock('Psr\Http\Message\UriInterface');
$uri->method('getPath')->willReturn('/foo/baz');
$uri->method('getQuery')->willReturn('');
$request = $this->createMock('Psr\Http\Message\RequestInterface');
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
$this->assertEquals('Hello World!', $this->chain->handle($request));
}
}
22、命令行模式(Command)
目的
为了封装调用和解耦。
我们有一个调用程序和一个接收器。 这种模式使用「命令行」将方法调用委托给接收器并且呈现相同的「执行」方法。 因此,调用程序只知道调用「执行」去处理客户端的命令。接收器会从调用程序中分离出来。
这个模式的另一面是取消方法的 execute (),也就是 undo () 。命令行也可以通过最小量的复制粘贴和依赖组合(不是继承)被聚合,从而组合成更复杂的命令集。
例子
文本编辑器:所有事件都是可以被解除、堆放,保存的命令。
Symfony2:SF2 命令可以从 CLI 运行,它的建立只需考虑到命令行模式。
大型 CLI 工具使用子程序来分发不同的任务并将它们封装在「模型」中,每个模块都可以通过命令行模式实现(例如:vagrant)。
UML图
代码
- CommandInterface.php
<?php
namespace DesignPatterns\Behavioral\Command;
interface CommandInterface
{
/**
* 这是在命令行模式中很重要的方法,
* 这个接收者会被载入构造器
*/
public function execute();
}
HelloCommand.php
<?php
namespace DesignPatterns\Behavioral\Command;
/**
* 这个具体命令,在接收器上调用 "print" ,
* 但是外部调用者只知道,这个是否可以执行。
*/
class HelloCommand implements CommandInterface
{
/**
* @var Receiver
*/
private $output;
/**
* 每个具体的命令都来自于不同的接收者。
* 这个可以是一个或者多个接收者,但是参数里必须是可以被执行的命令。
*
* @param Receiver $console
*/
public function __construct(Receiver $console)
{
$this->output = $console;
}
/**
* 执行和输出 "Hello World".
*/
public function execute()
{
// 有时候,这里没有接收者,并且这个命令执行所有工作。
$this->output->write('Hello World');
}
}
Receiver.php
<?php
namespace DesignPatterns\Behavioral\Command;
/**
* 接收方是特定的服务,有自己的 contract ,只能是具体的实例。
*/
class Receiver
{
/**
* @var bool
*/
private $enableDate = false;
/**
* @var string[]
*/
private $output = [];
/**
* @param string $str
*/
public function write(string $str)
{
if ($this->enableDate) {
$str .= ' ['.date('Y-m-d').']';
}
$this->output[] = $str;
}
public function getOutput(): string
{
return join("\n", $this->output);
}
/**
* 可以显示消息的时间
*/
public function enableDate()
{
$this->enableDate = true;
}
/**
* 禁止显示消息的时间
*/
public function disableDate()
{
$this->enableDate = false;
}
}
Invoker.php
<?php
namespace DesignPatterns\Behavioral\Command;
/**
*调用者使用这种命令。
* 比例 : 一个在 SF2 中的应用
*/
class Invoker
{
/**
* @var CommandInterface
*/
private $command;
/**
* 在这种调用者中,我们发现,订阅命令也是这种方法
* 还包括:堆栈、列表、集合等等
*
* @param CommandInterface $cmd
*/
public function setCommand(CommandInterface $cmd)
{
$this->command = $cmd;
}
/**
* 执行这个命令;
* 调用者也是用这个命令。
*/
public function run()
{
$this->command->execute();
}
}
测试
- Tests/CommandTest.php
<?php
namespace DesignPatterns\Behavioral\Command\Tests;
use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
use PHPUnit\Framework\TestCase;
class CommandTest extends TestCase
{
public function testInvocation()
{
$invoker = new Invoker();
$receiver = new Receiver();
$invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertEquals('Hello World', $receiver->getOutput());
}
}
23、迭代器模式(Iterator)
目的
让对象变得可迭代并表现得像对象集合。
例子
- 在文件中的所有行(对象表示形式的)上逐行处理文件(也是对象)
PHP 标准库 (SPL) 定义了一个最适合此模式的接口
迭代器!往往也需要实现 Countable 接口,允许在迭代器对象上使用
count($object)
方法。
UML
代码
- Book.php
<?php
namespace DesignPatterns\Behavioral\Iterator;
class Book
{
/**
* @var string
*/
private $author;
/**
* @var string
*/
private $title;
public function __construct(string $title, string $author)
{
$this->author = $author;
$this->title = $title;
}
public function getAuthor(): string
{
return $this->author;
}
public function getTitle(): string
{
return $this->title;
}
public function getAuthorAndTitle(): string
{
return $this->getTitle().' by '.$this->getAuthor();
}
}
BookList.php
<?php
namespace DesignPatterns\Behavioral\Iterator;
class BookList implements \Countable, \Iterator
{
/**
* @var Book[]
*/
private $books = [];
/**
* @var int
*/
private $currentIndex = 0;
public function addBook(Book $book)
{
$this->books[] = $book;
}
public function removeBook(Book $bookToRemove)
{
foreach ($this->books as $key => $book) {
if ($book->getAuthorAndTitle() === $bookToRemove->getAuthorAndTitle()) {
unset($this->books[$key]);
}
}
$this->books = array_values($this->books);
}
public function count(): int
{
return count($this->books);
}
public function current(): Book
{
return $this->books[$this->currentIndex];
}
public function key(): int
{
return $this->currentIndex;
}
public function next()
{
$this->currentIndex++;
}
public function rewind()
{
$this->currentIndex = 0;
}
public function valid(): bool
{
return isset($this->books[$this->currentIndex]);
}
}
测试
Tests/IteratorTest.php
<?php
namespace DesignPatterns\Behavioral\Iterator\Tests;
use DesignPatterns\Behavioral\Iterator\Book;
use DesignPatterns\Behavioral\Iterator\BookList;
use DesignPatterns\Behavioral\Iterator\BookListIterator;
use DesignPatterns\Behavioral\Iterator\BookListReverseIterator;
use PHPUnit\Framework\TestCase;
class IteratorTest extends TestCase
{
public function testCanIterateOverBookList()
{
$bookList = new BookList();
$bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
$bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
$bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
$books = [];
foreach ($bookList as $book) {
$books[] = $book->getAuthorAndTitle();
}
$this->assertEquals(
[
'Learning PHP Design Patterns by William Sanders',
'Professional Php Design Patterns by Aaron Saray',
'Clean Code by Robert C. Martin',
],
$books
);
}
public function testCanIterateOverBookListAfterRemovingBook()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$book2 = new Book('Professional Php Design Patterns', 'Aaron Saray');
$bookList = new BookList();
$bookList->addBook($book);
$bookList->addBook($book2);
$bookList->removeBook($book);
$books = [];
foreach ($bookList as $book) {
$books[] = $book->getAuthorAndTitle();
}
$this->assertEquals(
['Professional Php Design Patterns by Aaron Saray'],
$books
);
}
public function testCanAddBookToList()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$bookList = new BookList();
$bookList->addBook($book);
$this->assertCount(1, $bookList);
}
public function testCanRemoveBookFromList()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$bookList = new BookList();
$bookList->addBook($book);
$bookList->removeBook($book);
$this->assertCount(0, $bookList);
}
}