程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

面向对象和设计模式十四原型模式和桥接模式

面向对象和设计模式(十四)原型模式 和 桥接模式

本文介绍 原型模式 和 桥接模式,由于这两种模式比较短,因此放在一起介绍(我没有凑字数,我没有凑字数,我没有凑字数,狗头保命[旺柴])。

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、桥接模式的引入会增加系统的理解成本与设计难度。要求正确识别出系统中两个(或多个)独立变化的维度,因此其使用范围具有一定的局限性。


评论