
面向对象和设计模式(十五)装饰器模式——可以无限叠加功能的设计模式
01 装饰器模式
装饰器可以在不改变原始类( 原始类
又称为被装饰类)的情况下,通过在装饰器类中添加扩展功能的方式来给原始类附加额外功能,并且通过自由组合多个装饰器可以为原始类定制化添加多个功能。
装饰器模式的角色和类图:

抽象组件(Component):可以是一个接口或者抽象类,其充当被装饰类,规定了被装饰对象的行为;
具体组件(ConcreteComponent):实现/继承Component的一个具体对象,也即被装饰对象;
抽象装饰器(Decorator):通用的装饰ConcreteComponent的装饰器,其内部必然有一个属性指向Component抽象组件;其实现一般是一个抽象类,主要是为了让其子类按照其构造形式传入一个Component抽象组件,这是强制的通用行为(当然,如果系统中逻辑单一,并不需要实现许多装饰器,那么我们可以直接省略该类,而直接实现一个具体装饰器(ComcreteDecorator)即可);
具体装饰器(ConcreteDecorator):Decorator的具体实现类,理论上,每个ConcreteDecorator都扩展了Component对象的一种功能;
直接上例子说明装饰器模式的使用场景。
假如有一个日志处理类负责记录日志。
在某些业务场景下,只需要原原本本的记录日志到本地日志文件即可;
在某些业务场景下,需要将日志中的换行符转义再写入到本地日志文件;
在某些业务场景下,需要将日志进行json处理后再写入本地日志,且还需要将日志记录到云日志系统(如阿里云日志);
在某些业务场景下,每次写入日志都不是直接写入文件,而是先写入到内存的一个数组中,等这个数组内的日志内容积累到一定大小(例如1M)之后再将这一批日志一次性写入磁盘文件。
在任何业务中,记录到本地日志文件是必须的,最基本的能力。而需要什么附加的日志能力则由业务场景决定。
这时该怎么实现这个日志处理类?
最简单的实现方式就是创建一个抽象日志类作为父类,创建3个子类分别对应上述3个场景就OK了。
abstract class Logger{ public $stream; public function __construct(Writeable $stream){ // $stream 是一个可写的IO流对象,例如文件句柄对象、socket对象、db连接对象等等 $this->stream = $stream; }
abstract public function write(Log $log); // $log是一个日志对象,包含日志级别(info/error/notice)、日志内容、错误码等信息 }
// 本地文件日志类class FileLogger extends Logger{ public function __construct(Writeable $fileHandler){ // $fileHandler是文件句柄 $this->stream = $fileHandler; }
public function write(Log $log){ $this->stream->write($this->format($log)); }
public function format(Log $log){ $errno = $log->getErrno(); $level = $log->getLevel(); $errmsg = $log->getMsg();
return "errno:{$errno}; level:{$level}; errmsg:{$errmsg}\n"; }}
// 换行符格式化的本地文件日志类class FileWrapFormatterLogger extends FileLogger{ public function format(Log $log){ $cont = parent::format($log); return str_replace("\n", "\\n", $cont); }}
// json格式化且需写入云日志的本地文件日志类class FileJsonFormatterRemoteLogger extends FileLogger{ public $socket;
public function setSocket($socket){ $this->socket = $socket; }
public function write(Log $log){ $cont = $this->format($log); $this->stream->write($cont); $this->socket->send($cont); }
public function format(Log $log){ $errno = $log->getErrno(); $level = $log->getLevel(); $errmsg = json_encode($log->getMsg());
return "errno:{$errno}; level:{$level}; errmsg:{$errmsg}\n"; }}
// 具有缓冲能力的日志类 class BufferLogger extends FileLogger{ protected $buffer=[]; protected $curBufferSize = 0; protected $maxBufferSize;
public function setMaxBufferSize($size){ $this->maxBufferSize = $size; }
public function write(Log $log){ $logCnt = $this->format($log); $logSize = strlen($logCnt); $this->buffer[] = $logCnt; $this->curBufferSize += $logSize;
if($this->curBufferSize >= $this->maxBufferSize){ $bufferCnt = ''; foreach($this->buffer as $cnt){ $bufferCnt .= $cnt; } $this->stream->write($bufferCnt); $this->curBufferSize = 0; $this->buffer = []; } } }
假设之后随着业务的扩展,不同业务对日志记录的需求具有多样化,有些业务需要记录 本地文件日志+db日志+云日志、有的业务需要 本地文件日志+格式化+云日志 +
数据分析、有些业务需要
本地文件日志+redis缓存日志等等。为了满足所有的日志记录需求,使用继承的方式你需要实现的子类个数是排列组合的个数,扩展极不方便,类继承结构变得无比复杂。
这种情况就可以使用装饰器模式对其进行优化,做法是让装饰器类继承自被装饰类,装饰器类负责实现附加功能逻辑,每一种附加功能就是一个装饰器类,且该装饰器类需要依赖被装饰类或者其他兄弟装饰器类。装饰器类的核心方法中(例如上面例子中的write()方法)需要做两件事:一个是调用该装饰器类所实现的附加功能,二个是委托给被装饰类对象或者其他装饰器对象调用它们自己的核心方法。
装饰器类的一大特点就是可以叠加多个装饰器类,从而自由组合叠加不同的附加功能,其核心思想是“使用组合代替继承”,避免纯继承导致的“组合爆炸(组合数量过多,子类过多)”。
接下来用装饰器模式对上面的代码进行优化。
<?phpabstract class Logger{ public $stream; public function __construct(Writeable $stream){ // $stream 是一个可写的IO流对象,例如文件句柄对象、socket对象、db连接对象等等 $this->stream = $stream; } abstract public function write(Log $log); // $log是一个日志对象,包含日志级别(info/error/notice)、日志内容、错误码等信息 } // 本地文件日志类 class FileLogger extends Logger{ public function __construct(Writeable $fileHandler){ // $fileHandler是文件句柄 $this->stream = $fileHandler; } public function write(Log $log){ $this->stream->write($this->format($log)); } public function format(Log $log){ $errno = $log->getErrno(); $level = $log->getLevel(); $errmsg = $log->getMsg(); return "errno:{$errno}; level:{$level}; errmsg:{$errmsg}\n"; } } abstract class Decorator extends Logger{ // Logger是原始类 protected $logger; // 原始对象类或其他装饰器对象 public function __construct(Logger $logger) // 需要重写构造函数{ $this->logger = $logger; parent::__construct($logger->stream); } protected function beforeWrite(Log $log){ // to overwrite } protected function afterWrite(Log $log){ // to overwrite }
public function write(Log $log){ $this->beforeWrite($log); $this->logger->write($log); // 委托 $this->afterWrite($log); } }
// 换行符格式化的装饰器类 class FormatLoggerDecorator extends Decorator{ protected function beforeWrite(Log $log){ $logCnt = $log->getMsg(); $logCnt = str_replace("\n", "\\n", $logCnt); $log->setMsg($logCnt); } } // 写入云日志的装饰器类 class RemoteLoggerDecorator extends Decorator{ public $socket; public function setSocket($socket){ $this->socket = $socket; } protected function afterWrite(Log $log){ $this->socket->send($log->getMsg()); } } // 具有缓冲能力的日志类 class BufferLoggerDecorator extends Decorator{ protected $buffer=[]; protected $curBufferSize = 0; protected $maxBufferSize;
public function setMaxBufferSize($size){ $this->maxBufferSize = $size; }
protected function beforeWrite(Log $log){ $logCnt = $log->getMsg(); $logSize = strlen($logCnt); $this->buffer[] = $logCnt; $this->curBufferSize += $logSize; }
public function write(Log $log){ // 重写write方法 $this->beforeWrite($log); if($this->curBufferSize >= $this->maxBufferSize){ $bufferCnt = ''; foreach($this->buffer as $cnt){ $bufferCnt .= $cnt; } $this->stream->write($bufferCnt); $this->curBufferSize = 0; $this->buffer = []; } } }
// 假如我同时需要将换行符格式化、缓冲日志 和 写入远程日志可以这样做$stream = ...; $socket = ...;$log = ...;$logger = new FileLogger($stream);$logger = new FormatDecorator($logger);$logger = new RemoteLoggerDecorator($logger);$logger->setSocket($socket);$logger = new BufferLoggerDecorator($logger);$logger->write($log);
上面的代码中,beforeWrite和afterWrite方法就是装饰器附加功能逻辑实现的地方。当然啦,装饰器不一定非得有 before 和 after
方法,也可以直接将附加功能直接写到装饰器子类的write方法中,这样子反而更灵活。
public function write(Log $log){ // ... $this->logger->write($log); // ...}
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类增强自身功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类(被装饰类)继承相同的抽象类或者接口。
02 装饰器模式和代理模式的区别
不清楚或者忘记了代理模式的朋友们,下面是传送门: 面向对象和设计模式(十三)代理模式与动态代理模式
装饰器模式和代理模式的区别如下:
1、目的和含义不同:装饰器模式强调增强自身,装饰器中添加的功能是些和被装饰对象业务相关的功能;代理模式强调对对象的控制,要代理类帮被代理类做一些与被代理类业务无关的职责(记录日志、设置缓存)。
2、对上层调用而言是否透明:代理模式对被代理类的控制是透明的,上层调用者使用代理类的方法时能感受到的还像是使用被代理类的方法一样。装饰器模式则相反,使用什么装饰器对基础类装饰、装饰出来的对象应该具有什么功能,是由上层调用和业务决定的,因此各个装饰器对上层调用者是不透明的,或者说上层调用者需要知道每个装饰器的作用。
3、从形式上来说:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类达到功能叠加的效果,代理类则没有和被代理类继承同一个父类。