
面向对象和设计模式(十四)原型模式 和 桥接模式
本文介绍 原型模式 和 桥接模式,由于这两种模式比较短,因此放在一起介绍(我没有凑字数,我没有凑字数,我没有凑字数,狗头保命[旺柴])。
01 原型模式
什么是原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分成员属性都相同),可以利用对已有对象(也就是原型)进行拷贝的方式来创建新对象,以达到节省创建时间的目的,这就是原型模式。
原型模式的使用场景:
如果想要获取一个和原有对象状态一致(也就是数据一致且属性一致)的新对象,但是遇到以下问题的情况下,就可以使用原型模式。
1、对象中的数据需要复杂的计算才能得到,如排序、计算哈希值等,或者从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取。
拷贝对象可以跳过这些过程和开销,直接得到这些数据。
2、对象达到当前状态需要调用很多过程和方法调用,为了避免调用者重复执行这些调用,可以直接拷贝对象一步到位。
那么我们刚刚说了原型模式在拷贝对象上游优势,可是什么情况下我们会想要获得一个和原有对象一样的新对象呢?
举个例子,当一个对象A需要提供给对象B和对象C访问,
而且B和C都可能需要修改对象A内部的值,如果不想B对A的变更影响到C对A的使用,就可以克隆出另一个对象A,让B和C分别依赖两个相互独立的对象A。
原型模式如何实现
只要在类中实现一个方法,使得对象调用这个方法后能得到一个和原对象一模一样的新对象,我们就可以说这个类实现了原型模式。
在讨论如何实现之前,需要知道什么是深拷贝和浅拷贝。
我们知道,将一个对象存储到变量中,这个变量其实存储的是对象的引用,或者说存的是对象在内存中的地址。当把这个变量赋值给另一个变量的时候,两个变量存储的都是对象的地址,对象在内存空间中只有一份。
同样的道理,如果一个对象A里面的成员属性既有标量类型,又有对象类型,那么对象A也只是持有对象成员的引用。
假设有一个Row 行对象,里面包含2个字段成员属性,一个是标量成员 Id,一个是对象成员 Field_A(字段A)。
Row对象拷贝出一个新对象Row,且两个Row对象的Field_A成员指向的是内存空间的同一个内容(同一个地址),那么说明 Field_A
对象没有被拷贝,依旧只有一份,这种情况就是浅拷贝。
简单的来说,浅拷贝就是值拷贝了表层的对象 Row ,没有拷贝对象Row内部的对象 Field_A。
对浅拷贝出来的新对象的变更依旧可能会影响原对象。

如果 拷贝对象Row 的时候,里面的 Field_A 对象也拷贝了一份,那么这种情况就是深拷贝。

浅拷贝的实现方式在各个语言中基本都有方法和函数直接提供,例如python中的copy.copy()、java中Object 类的 clone()
方法、PHP中的clone关键字。
深拷贝则有两种实现方式:
一种是递归的对对象进行浅拷贝,直到要拷贝的最内层对象只包含基本数据类型数据,没有引用对象为止。
另一种最简单粗暴实用,就是直接对对象进行序列化,再对其反序列化得到新对象。
原型模式下,基本都是以深拷贝的方式进行对象拷贝,否则对 拷贝对象 的更改依旧会 影响到 原对象,原型模式就失去了意义。
下面以PHP语言为例。
浅拷贝示例
PHP 中可以使用 clone 关键字进行浅拷贝的克隆对象。
<?phpclass Inner{ public $i = 0;
public function __construct($i){ $this->i = $i; }
public function incr(){ $this->i++; }
public function getInt(){ return $this->i; }}
class Test{ public $name; public $inner;
public function __construct($name, Inner $inner){ $this->name = $name; $this->inner = $inner; }
public function getInner(){ return $this->inner; }}
$inner = new Inner(10);$t1 = new Test('zbp',$inner);
$t2 = clone $t1; // 浅拷贝$t2->getInner()->incr();
var_dump($t1->getInner()->getInt()); // 11var_dump($t2->getInner()->getInt()); // 还是11 说明t1和t2中的inner对象是同一个inner对象
一般而言,如果我们在克隆对象之后还希望自动对新对象的属性做一些变更,可以通过重写__clone()方法来做到。
如果在类中设置一个空的,且访问权限为 private 的 __clone()
方法的话,可以起到禁止通过clone关键字克隆的作用。当然,这个方法无法禁止通过 serialize()序列化函数 和 unserialize()
反序列化函数 实现的深拷贝。
private function __clone(){
}
深拷贝示例
PHP 中可以使用 serialize()序列化函数 和 unserialize() 反序列化函数实现深拷贝。
$inner = new Inner(10);$t1 = new Test('zbp',$inner);
$t2 = unserialize(serialize($t1));$t2->getInner()->incr();
var_dump($t1->getInner()->getInt()); // 10var_dump($t2->getInner()->getInt()); // 11 说明t1和t2中的inner对象不是同一个inner对象,而是相互独立的两个对象
02 桥接模式
什么是桥接模式
网上对桥接模式的解释有两种。
第一种理解方式是将抽象和实现解耦,让它们可以独立变化。
说具体一点就是将抽象和实现完全分开,“抽象”指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码负责流程控制和调度,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系组装在一起。
另一种理解方式是当一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展,而不是将多个维度包含在一个类中,导致继承的子类指数级增长。
举个例子,有一个画笔类,它有两个维度的概念:颜色和种类。
颜色有:红色、黑色、白色、黄色;种类有:蜡笔、毛笔、铅笔。通过继承的方式来实现这个需求,需要写3*4=12个子类。
但如果将颜色作为一个维度的类,将种类作为一个维度的类,只需要实现3+4=7个类,然后通过将这些类组合起来,实现最终的画笔类。
接下来我们看看桥接模式包含的角色以及类图。

抽象化(Abstraction)角色:抽象化角色是个抽象类,提供高层控制逻辑,依赖于一个实现化对象的引用来完成底层实际工作。
扩展抽象化(Refined Abstraction)角色:是抽象化角色的实现。
实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化(Concrete Implementor)角色:实现化角色接口的具体实现。
客户端(Client):仅关心如何与抽象部分合作。但是,客户端需要将抽象对象与一个实现对象连接起来。
一般来说,抽象化角色用来承载主维度,实现化角色用来承载其他维度。就像上面我提到的画笔类的例子,种类这个维度就可以作为抽象化角色,衍化出蜡笔类、毛笔类、铅笔类这3个扩展抽象化类;
颜色这个维度可以作为实现化角色,衍化出红色类、黑色类、白色类、黄色类这4个具体实现类;
如果再多扩展一个维度:品牌,那么就可以再定义一个品牌维度的实现化角色类。
然后在每个扩展抽象化类(“种类”类)中引用 颜色类 和 品牌类 的方法。
桥接模式的适用场景
当一个对象有多个变化因素的时候,需要将不同维度的具体实现分离,避免继承带来的组合爆炸。如手机品牌有2种变化因素,一个是品牌,一个是功能。
当控制流程和具体实现需要分开来独立的变化时,可以使用桥接模式。
下面我们来看一个例子,有一个 API 接口监控告警 系统。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。
通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。
不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员。
最简单的实现方式如下:
<?php
class NotificationEmergencyLevel { const SEVERE = 1; const URGENCY = 2; const NORMAL = 3; const TRIVIAL = 4; } class Notification { private $emailAddresses = []; private $telephones = []; private $wechatIds = []; public function setEmailAddress($emailAddress) { $this->emailAddresses = $emailAddress; } public function setTelephones($telephones) { $this->telephones = $telephones; } public function setWechatIds($wechatIds) { $this->wechatIds = $wechatIds; } public function notify($level, $message) { if ($level == (NotificationEmergencyLevel::SEVERE)) { //…自动语音电话 } else if ($level == NotificationEmergencyLevel::URGENCY) { //…发微信 } else if ($level == NotificationEmergencyLevel::NORMAL) { //…发邮件 } else if ($level == NotificationEmergencyLevel::TRIVIAL) { //…发邮件 } } }
在这个例子中,变化因素有通知方式和告警级别两个维度,通知方式取决于告警级别。我们可以使用桥接模式对其进行优化,以Notification作为抽象化角色承载告警级别这一维度,以新类MessageSender作为实现化角色承载通知方式这一维度,再将其组合。
<?php
class NotificationEmergencyLevel { const SEVERE = 1; const URGENCY = 2; const NORMAL = 3; const TRIVIAL = 4; } abstract class Notification { protected $messageSender;
public function __construct(MessageSender $messageSender){ $this->messageSender = $messageSender; }
abstract public function notify($message); }
class SevereNotification extends Notification { public function __construct(MessageSender $msgSender) { parent::__construct($msgSender); }
public function notify($message) { // ...体现告警等级差异的代码 $this->messageSender->send($message); } }
class UrgencyNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } class NormalNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } class TrivialNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... }
interface MessageSender{ public function send($message); }
class TelephoneMsgSender implements MessageSender{ private $telephones = [];
public function setTelephones($telephones) { $this->telephones = $telephones; }
public function send($message){ // …具体实现 } }
class EmailMsgSender implements MessageSender{ // 与TelephoneMsgSender代码结构类似,所以省略… }
class WechatMsgSender implements MessageSender { // 与TelephoneMsgSender代码结构类似,所以省略… }
这样一来因告警级别而异的具体实现可以在Notification的子类完成,因通知方式而异的具体实现可以在MessageSender类完成。
桥接模式优缺点
优点
1、分离抽象接口及其实现部分。
2、桥接模式通过组合替代继承的方式,避免随着维度增多导致继承的子类数量过多。
3、桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
缺点
1、桥接模式的引入会增加系统的理解成本与设计难度。要求正确识别出系统中两个(或多个)独立变化的维度,因此其使用范围具有一定的局限性。