一、桥接模式概念
网上对桥接模式的解释有两种。
第一种理解方式是将抽象和实现解耦,让它们可以独立变化。
说具体一点就是将抽象和实现完全分开,“抽象”指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码负责流程控制和调度,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系组装在一起。
另一种理解方式是当一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展,而不是将多个维度包含在一个类中,导致继承的子类指数级增长。
举个例子,有一个画笔类,它有两个维度的概念:颜色和种类。颜色有:红色、黑色、白色、黄色;种类有:蜡笔、毛笔、铅笔。通过继承的方式来实现这个需求,需要写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、桥接模式的引入会增加系统的理解成本与设计难度。要求正确识别出系统中两个(多个)独立变化的维度,因此其使用范围具有一定的局限性。