那些刚接触软件开发的人倾向于集中精力掌握编程语言。一旦跨越了这一障碍,就应该拥抱设计模式,因为没有它们,编写高质量和复杂的软件几乎是不可能的。设计模式主要归功于经验丰富的开发人员使用,它代表了解决我们应用程序中常见挑战的成熟解决方案。成功应用设计模式可能会产生更具可扩展性、可重用性、可维护性和适应性的代码。
本章中的示例不可复制和粘贴。它们仅用于表示设计模式的一种可能实现。毕竟,现实生活中的应用程序都是关于细节的。此外,还有很多其他的设计模式,随着技术和编程范式的转变,新的设计模式正在被发明。
在本章中,我们将介绍几种可能的 PHP 设计模式实现:
- 基本模式
- 注册表模式
- 创建模式
- 单一模式
- 原型模式
- 抽象工厂模式
- 构建器模式
- 对象池模式
- 行为模式
- 战略模式
- 观察者模式
- 惰性初始化模式
- 责任链模式
- 结构模式
- 装饰图案
在下一节中,我们将了解基本模式:注册表模式。
注册表模式是一个有趣的模式,它允许我们存储和检索对象以供以后使用。存储和检索的过程基于我们定义的键。根据数据范围,键和对象的关联在进程、线程或会话中是全局的,允许我们从数据范围内的任何位置检索对象。
以下示例演示了可能的注册表模式实现:
<?php
class Registry
{
private
$registry = [];
public
function get($key)
{
if (isset($this->registry[$key])) {
return $this->registry[$key];
}
return null;
}
public
function set($key, $value, $graceful = false)
{
if (isset($this->registry[$key])) {
if ($graceful) {
return;
}
throw new \RuntimeException('Registry key "' . $key . '"already exists');
}
$this->registry[$key] = $value;
}
public
function remove($key)
{
if (isset($this->registry[$key])) {
unset($this->registry[$key]);
}
}
public
function __destruct()
{
$keys = array_keys($this->registry);
array_walk($keys, [$this, 'remove']);
}
}
// Client use
class User
{
public $name;
}
$user1 = new User();
$user1->name = 'John';
$user2 = new User();
$user2->name = 'Marc';
$registry = new Registry();
$registry->set('employee', $user1);
$registry->set('director', $user2);
echo $registry->get('director')->name; // Marc
我们的Registry
类实现有三种关键方法:get()
、set()
、remove()
。set()
方法允许基于$graceful
参数的优雅行为;否则会触发现有密钥的RuntimeException
。我们还定义了一个__destruct
方法,作为一种清理机制,在$registry
实例被销毁时删除注册表中的每个项。
在本节中,我们将了解一些创造性的模式,例如单例模式、原型模式、抽象工厂模式和构建器模式。
singleton 是大多数开发人员最先学习的设计模式之一。此设计模式的目标是将类实例化的数量限制为仅一个。这意味着对类使用new
关键字将始终返回一个相同的对象实例。这是一个强大的概念,它允许我们实现各种应用程序范围的对象,如记录器、邮件器、注册表,以及我们可能希望充当单例的其他功能。但是,我们很快就会看到,我们将完全避免使用new
关键字,并通过静态类方法实例化一个对象。
以下示例演示了一种可能的单例模式实现:
<?php
class Logger
{
private static $instance;
const TYPE_ERROR = 'error';
const TYPE_WARNING = 'warning';
const TYPE_NOTICE = 'notice';
protected function __construct()
{
// empty?!
}
private function __clone()
{
// empty?!
}
private function __wakeup()
{
// empty?!
}
public static function getInstance()
{
if (!isset(self::$instance)) {
// late static binding
self::$instance = new self;
}
return self::$instance;
}
public function log($type, $message)
{
return sprintf('Logging %s: %s', $type, $message);
}
}
// Client use
echo Logger::getInstance()->log(Logger::TYPE_NOTICE, 'test');
根据getInstance()
方法的实现,Logger
类使用静态成员$instance
保存一个self
的实例。为了防止通过new
操作符创建新实例,我们将__construct
定义为protected
。__clone()
方法被定义为private
,以防止通过clone
操作符克隆实例。类似地,__wakeup()
方法也被定义为 private,以防止通过unserialize()
函数对实例进行非序列化。这几个简单的限制使类成为一个单独的类。为了获取实例,只需调用getInstance()
类方法。
原型模式是通过克隆来创建新对象。这是一个很好的概念,因为我们不再使用new
关键字来创建新对象。PHP 语言提供了一个特殊的clone
关键字来帮助对象克隆。
以下示例演示了一种可能的原型模式实现:
<?php
class Logger
{
public $channel = 'N/A';
}
class SystemLogger extends Logger
{
public function __construct()
{
$this->channel = 'STDIN';
}
public function log($data)
{
return sprintf('Logging %s to %s.', $data, $this->channel);
}
public function __clone()
{
/* additional changes for (after)clone behavior? */
}
}
// Client use
$systemLogger = new SystemLogger();
echo $systemLogger->log('test');
$logger = clone $systemLogger;
echo $logger->log('test2');
$logger->channel = 'mail';
echo $logger->log('test3');
// Logging test to STDIN.
// Logging test2 to STDIN.
// Logging test3 to mail.
通常,克隆一个对象所需要的只是使用一个表达式,比如$clonedObj = clone $obj;
。然而,这并不能让我们控制克隆过程。PHP 对象可能很重,有很多成员和引用。有时,我们希望对克隆对象施加某些限制。这就是神奇的__clone()
方法派上用场的地方。__clone()
方法在克隆过程完成后触发,这是可能的清理代码实现需要记住的。
抽象工厂封装了一组具有公共功能的单个工厂。它这样做没有指定它们的具体类。这使得编写可移植代码变得更容易,因为客户端可以在不更改代码的情况下交换具体的实现。
以下示例演示了可能的抽象工厂模式实现:
<?php
interface Button
{
public function render();
}
interface FormFactory
{
public function createButton();
}
class LoginButton implements Button
{
public function render()
{
return '<button name="login">Login</button>';
}
}
class RegisterButton implements Button
{
public function render()
{
return '<button name="register">Register</button>';
}
}
class LoginFactory implements FormFactory
{
public function createButton()
{
return new LoginButton();
}
}
class RegisterFactory implements FormFactory
{
public function createButton()
{
return new RegisterButton();
}
}
// Client
$loginButtonFactory = new LoginFactory();
$button = $loginButtonFactory->createButton();
echo $button->render();
$registerButtonFactory = new RegisterFactory();
$button = $registerButtonFactory->createButton();
echo $button->render();
我们首先创建了两个简单的接口,Button
和FormFactory
。Button
接口定义了一个render()
方法,然后我们通过两个具体的类实现LoginButton
和RegisterButton
来实现。两个FormFactory
实现LoginFactory
和RegisterFactory
然后实例化相应的按钮类,作为其createButton()
方法实现的一部分。客户端只使用LoginFactory
和RegisterFactory
实例,因此避免直接实例化具体的按钮类。
构建器模式非常方便,尤其是在大型应用程序中。它将复杂对象的构造与其表示分离。这使得相同的构造过程可以创建多个表示。
下面的示例以Image
类为例演示了一个可能的生成器模式实现:
<?php
class Image
{
private $width;
private $height;
public function getWidth()
{
return $this->width;
}
public function setWidth($width)
{
$this->width = $width;
return $this;
}
public function getHeight()
{
return $this->height;
}
public function setHeight($height)
{
$this->height = $height;
return $this;
}
}
interface ImageBuilderInterface
{
public function setWidth($width);
public function setHeight($height);
public function getResult();
}
class ImageBuilder implements ImageBuilderInterface
{
private $image;
public function __construct()
{
$this->image = new Image();
}
public function setWidth($width)
{
$this->image->setWidth($width);
return $this;
}
public function setHeight($height)
{
$this->image->setHeight($height);
return $this;
}
public function getResult()
{
return $this->image;
}
}
class ImageBuildDirector
{
private $builder;
public function __construct(ImageBuilder $builder)
{
$this->builder = $builder;
}
public function build()
{
$this->builder->setWidth(120);
$this->builder->setHeight(80);
return $this;
}
public function getImage()
{
return $this->builder->getResult();
}
}
// Client use
$imageBuilder = new ImageBuilder();
$imageBuildDirector = new ImageBuildDirector($imageBuilder);
$image = $imageBuildDirector->build()->getImage();
var_dump($image);
// object(Image)#2 (2) { ["width":"Image":private]=> int(120) ["height":"Image":private]=> int(80) }
我们从一个简单的 Image 类开始,该类提供宽度和高度属性以及相应的 getter 和 setter。然后我们创建了ImageBuilderInterface
接口,该接口定义了图像宽度和高度设置方法以及getResult()
方法。然后,我们创建了一个实现ImageBuilderInterface
接口的ImageBuilder
具体类。客户端实例化ImageBuilder
类。另一个具体类ImageBuildDirector
通过使用通过其构造函数传递的ImageBuilder
实例,将创建代码或生成器代码封装在其build()
方法中。
对象池模式管理类实例——对象。它用于由于资源密集型操作而限制不必要的类实例化的情况。对象池的作用非常类似于对象的注册表,客户机可以稍后从中提取必要的对象。
以下示例演示了一种可能的对象池模式实现:
<?php
class ObjectPool
{
private $instances = [];
public function load($key)
{
return $this->instances[$key];
}
public function save($object, $key)
{
$this->instances[$key] = $object;
}
}
class User
{
public function hello($name)
{
return 'Hello ' . $name;
}
}
// Client use
$pool = new ObjectPool();
$user = new User();
$key = spl_object_hash($user);
$pool->save($user, $key);
// code...
$user = $pool->load($key);
echo $user->hello('John');
只使用一个数组和两个方法,我们就能够实现一个简单的对象池。save()
方法将对象添加到$instances
数组,而load()
方法将对象返回给客户端。在本例中,客户机负责跟踪保存对象的密钥。对象本身在使用后不会被销毁,因为它们仍保留在池中。
在本节中,我们将介绍行为模式,如策略、观察者、惰性初始化和责任链
当我们有多个执行类似操作的代码块时,策略模式就派上了用场。它定义了一个封装的、可互换的算法系列。想象一下,在订单结帐过程中,我们希望实现不同的发货提供商,如 UPS 和 FedEx。
以下示例演示了一种可能的策略模式实现:
<?php
interface ShipmentStrategy
{
public function calculate($amount);
}
class UPSShipment implements ShipmentStrategy
{
public function calculate($amount)
{
return 'UPSShipment...';
}
}
class FedExShipment implements ShipmentStrategy
{
public function calculate($amount)
{
return 'FedExShipment...';
}
}
class Checkout
{
private $amount = 0;
public function __construct($amount = 0)
{
$this->amount = $amount;
}
public function estimateShipment()
{
if ($this->amount > 199.99) {
$shipment = new FedExShipment();
} else {
$shipment = new UPSShipment();
}
return $shipment->calculate($this->amount);
}
}
// Client use
$checkout = new Checkout(19.99);
echo $checkout->estimateShipment(); // UPSShipment...
$checkout = new Checkout(499.99);
echo $checkout->estimateShipment(); // FedExShipment...
我们首先用一个calculate()
方法定义一个ShipmentStrategy
接口。然后我们定义了实现ShipmentStrategy
接口的UPSShipment
和FedExShipment
类。有了这两个具体的装运类,我们创建了一个Checkout
类,将这两个装运选项封装在其estimateShipment()
方法中。然后,客户机在Checkout
实例上调用estimateShipment()
方法。根据传递的金额,将进行不同的装运计算。使用此模式,我们可以在不更改客户端的情况下自由添加新的装运计算。
观察者模式是一种非常流行的模式。它允许事件订阅类型的行为。我们区分对象的主体和观察者类型。观察者是一个订阅了主体-对象状态变化的对象。当主体改变其状态时,它会自动通知所有观察者。
以下示例演示了一种可能的观察者模式实现:
<?php
class CheckoutSuccess implements \SplSubject
{
protected $salesOrder;
protected $observers;
public function __construct($salesOrder)
{
$this->salesOrder = $salesOrder;
$this->observers = new \SplObjectStorage();
}
public function attach(\SplObserver $observer)
{
$this->observers->attach($observer);
}
public function detach(\SplObserver $observer)
{
$this->observers->detach($observer);
}
public function notify()
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function getSalesOrder()
{
return $this->salesOrder;
}
}
class SalesOrder
{
}
class Mailer implements \SplObserver
{
public function update(\SplSubject $subject)
{
echo 'Mailing ', get_class($subject->getSalesOrder()), PHP_EOL;
}
}
class Logger implements \SplObserver
{
public function update(\SplSubject $subject)
{
echo 'Logging ', get_class($subject->getSalesOrder()), PHP_EOL;
}
}
$salesOrder = new SalesOrder();
$checkoutSuccess = new CheckoutSuccess($salesOrder);
// some code...
$checkoutSuccess->attach(new Mailer());
// some code...
$checkoutSuccess->attach(new Logger());
// some code...
$checkoutSuccess->notify();
PHP\SplSubject
和\SplObserver
接口允许观察者模式实现。我们的 checkout success 示例使用这些接口来实现CheckoutSuccess
作为对象的主题类型的类,以及Mailer
和Logger
作为对象的观察者类型的类。使用CheckoutSuccess
实例的attach()
方法,我们将两个观察者都附加到该对象上。一旦调用 subjectnotify()
方法,个体观察者update()
方法就会被触发。getSalesOrder()
方法调用可能会令人惊讶,因为SplSubject
对象的直接实例上没有实际的getSalesOrder()
方法。但是,我们示例中的两个update(\SplSubject $subject)
方法调用将接收CheckoutSuccess
的实例。否则,将$subject
参数直接转换为CheckoutSuccess
将导致 PHP 致命错误,如下所示。
PHP Fatal error: Declaration of Logger::update(CheckoutSuccess $subject) must be compatible with SplObserver::update(SplSubject $SplSubject)
惰性初始化模式对于寻址实例化可能占用大量资源的对象非常有用。其想法是延迟实际的资源密集型操作,直到实际需要其结果。PDF 生成是一个轻到中度资源密集型操作的示例。
以下示例演示了一种基于 PDF 生成的延迟初始化模式实现:
<?php
interface PdfInterface
{
public function generate();
}
class Pdf implements PdfInterface
{
private $data;
public function __construct($data)
{
$this->data = $data;
// Imagine resource intensive pdf generation here
sleep(3);
}
public function generate()
{
echo 'pdf: ' . $this->data;
}
}
class ProxyPdf implements PdfInterface
{
private $pdf = null;
private $data;
public function __construct($data)
{
$this->data = $data;
}
public function generate()
{
if (is_null($this->pdf)) {
$this->pdf = new Pdf($this->data);
}
$this->pdf->generate();
}
}
// Client
$pdf = new Pdf('<h1>Hello</h1>'); // 3 seconds
// Some other code ...
$pdf->generate();
$pdf = new ProxyPdf('<h1>Hello</h1>'); // 0 seconds
// Some other code ...
$pdf->generate();
根据类的构造方式,它可能会在调用new
关键字之后触发实际生成,就像我们对new Pdf(...)
表达式所做的那样。new ProxyPdf(...)
表达式的行为不同,因为它围绕着实现相同PdfInterface
的Pdf
类,但提供了不同的__construct()
方法实现。
责任链模式允许我们以发送方-接收方的方式链接代码,而这两者是相互解耦的。这使得有多个对象处理传入请求成为可能。
以下示例以记录器功能为例演示了可能的责任链模式实现:
<?php
abstract class Logger
{
private $logNext = null;
public function logNext(Logger $logger)
{
$this->logNext = $logger;
return $this->logNext;
}
final public function push($message)
{
$this->log($message);
if ($this->logNext !== null) {
$this->logNext->push($message);
}
}
abstract protected function log($message);
}
class SystemLogger extends Logger
{
public function log($message)
{
echo 'SystemLogger log!', PHP_EOL;
}
}
class ElasticLogger extends Logger
{
protected function log($message)
{
echo 'ElasticLogger log!', PHP_EOL;
}
}
class MailLogger extends Logger
{
protected function log($message)
{
echo 'MailLogger log!', PHP_EOL;
}
}
// Client use
$systemLogger = new SystemLogger();
$elasticLogger = new ElasticLogger();
$mailLogger = new MailLogger();
$systemLogger
->logNext($elasticLogger)
->logNext($mailLogger);
$systemLogger->push('Stuff to log...');
//SystemLogger log!
//ElasticLogger log!
//MailLogger log!
我们首先创建了一个抽象的Logger
类,它有三种方法:logNext()
、push()
和log()
。log()
方法被定义为抽象,这意味着实现留给子类。logNext()
方法是关键的组成部分,因为它将对象向下移动。然后我们创建了Logger
类的三个具体实现:SystemLogger
、ElasticLogger
和MailLogger
。然后,我们实例化了一个具体的 logger 类,并使用logNext()
方法向下传递了另外两个实例。最后,我们调用push()
方法来触发该链。
在本节中,我们将了解一种结构模式:decorator 模式。
装饰图案很简单。它允许我们在不影响同一类的其他实例的情况下向对象实例添加新行为。它基本上充当对象周围的装饰包装器。我们可以想象一个具有 Logger 类实例的简单用例,其中我们有一个简单的 Logger 类,我们希望偶尔对其进行修饰,或者将其包装成更具体的错误、警告和注意级别的 Logger。
以下示例演示了一种可能的装饰器模式实现:
<?php
interface LoggerInterface
{
public function log($message);
}
class Logger implements LoggerInterface
{
public function log($message)
{
file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
}
}
abstract class LoggerDecorator implements LoggerInterface
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
abstract public function log($message);
}
class ErrorLogger extends LoggerDecorator
{
public function log($message)
{
$this->logger->log('ErrorLogger: ' . $message);
}
}
class WarningLogger extends LoggerDecorator
{
public function log($message)
{
$this->logger->log('WarningLogger: ' . $message);
}
}
class NoticeLogger extends LoggerDecorator
{
public function log($message)
{
$this->logger->log('NoticeLogger: ' . $message);
}
}
// Client use
(new Logger())->log('Test Logger.');
(new ErrorLogger(new Logger()))->log('Test ErrorLogger.');
(new WarningLogger(new Logger()))->log('Test WarningLogger.');
(new NoticeLogger(new Logger()))->log('Test NoticeLogger.');
在这里,我们首先定义一个LoggerInterface
接口和一个实现该接口的具体Logger
类。然后我们创建了一个abstract``LoggerDecorator
类,该类也实现了LoggerInterface
。LoggerDecorator
没有真正实现log()
方法本身;它将其定义为abstract
供将来的子类实现。最后,我们定义了具体的错误、警告和通知装饰器类。我们可以看到它们的log()
方法根据它们的角色装饰输出。结果输出如下所示:
Test Logger.
ErrorLogger: Test ErrorLogger.
WarningLogger: Test WarningLogger.
NoticeLogger: Test NoticeLogger.
在本章中,我们介绍了 PHP 应用程序中使用的一些最常见的设计模式。由于还有其他可用的设计模式,该列表远非最终结果。虽然有些设计模式非常通用,但其他模式可能更适合 GUI 或应用程序编程的其他领域。了解如何使用和应用设计模式可以使我们的代码更具可扩展性、可重用性、可维护性和适应性。
接下来,我们将更详细地了解如何使用 SOAP、REST 和 Apache Thrift 构建 web 服务。