设计模式之桥接(Bridge)模式
意图
桥接模式(Bridge)是一种结构型设计模式,可以将一个大型类或一组密切相关的类拆分为两个独立的层次结构——抽象和实现——它们可以相互独立地开发。
问题描述
抽象?实现?听起来很吓人?请保持冷静,我们来思考一个简单的示例。
假设你有一个几何形状 shape
类,它有一对子类:Circle
和 Square
。你想扩展这个类层次结构以合并颜色,所以你计划创建 Red
和 Blue
形状子类。但是,由于您已经有两个子类,因此需要创建四个类组合,如 BlueCircle
和 RedSquare
。
向层次结构中添加新的形状类型和颜色将使其呈指数级增长。例如,要添加三角形,您需要引入两个子类,每种颜色一个子类。之后,添加新颜色需要创建三个子类,每种形状类型一个子类。我们走得越远,情况就越糟。
方案
出现这个问题是因为我们试图在两个独立的维度上扩展形状类:按形状和按颜色。这是类继承中非常常见的问题。
Bridge 模式试图通过从继承切换到对象组合来解决这个问题。这意味着将其中一个维度提取到一个单独的类层次结构中,这样原始类将引用新层次结构的对象,而不是将其所有状态和行为都包含在一个类中。
按照这种方法,我们可以将与颜色相关的代码提取到具有两个子类的类中:Red
和 Blue
。然后,Shape
类获得一个指向其中一个颜色对象的引用字段。现在,该形状可以将任何与颜色相关的工作委托给连接的颜色对象。该引用将充当 Shape
和 Color
类之间的桥梁。从现在起,添加新颜色将不需要更改形状层次结构,反之亦然。
抽象和实现
GoF 书引入术语抽象和实现作为桥接定义的一部分。在我看来,这些术语听起来过于学术且使得该模式看起来比真实情况更为复杂。读了使用形状和颜色的简单示例后,让我们来解读GoF书中可怕的单词背后的含义。
抽象 (也叫接口)是一些实体的高级控制层。该层本身不应该做任何真正的工作。它应该将工作委托给实现层(也称为平台)。 is a high-level
请注意,我们谈论的编程语言中的接口或抽象类。这些东西不一样。
在讨论实际应用程序时,抽象可以用图形用户界面(GUI)表示,实现可以是 GUI 层响应用户交互而调用的底层操作系统代码(API)。
一般来说,你可以在两个独立的方向上扩展这样的应用程序:
- 有多个不同的 GUI(比如,为老客户或管理员量身定制)。
- 支持多个不同的 API (比如,使之能在 Windows、Linux 和 macOS 上启动应用)。
在最坏的情况下,这个应用程序可能看起来像一个巨大的意大利面条碗,数百个条件语句将不同类型的 GUI 与代码中的各种 API 连接起来。
你可以将与特定接口平台组合相关的代码提取到单独的类中,从而使这种混乱有序。然而,很快你就会发现有很多这样的类。类层次结构将呈指数级增长,因为添加新的 GUI 或支持不同的 API 将需要创建越来越多的类。
让我们尝试使用 Bridge 模式来解决这个问题。这表明我们将类划分为两个层次结构:
- 抽象:应用的 GUI 层。
- 实现:操作系统的 API。
抽象对象控制应用程序的外观,将实际工作委派给链接的实现对象。不同的实现是可互换的,只要它们遵循一个通用接口,使同一个 GUI 能够在 Windows 和 Linux 下工作。
因此,你可以在不接触与 API 相关的类的情况下更改 GUI 类。此外,添加对另一个操作系统的支持只需要在实现层次结构中创建一个子类。
结构
- 抽象类(Abstraction )提供了高级控制逻辑。他依赖于实现对象来世界处理低级别的工作。
- 实现类(Implementation)声明了所有具体实现通用的接口。一个抽象类通过此处声明的方法只与一个实现对象通信。抽象类可能罗列了与实现类相同的方法,不过通常抽象类声明了一些复杂的行为,这些行为依赖于实现声明的各种基元操作。
- 具体实现类(Concrete Implementation)包含了特定平台的代码。
- Refined Abstraction 提供了控制逻辑的变体。与其父类一样,他们通过通用的实现接口处理不同的实现。
- 通常,客户端(Client)只处理抽象类。不过,该客户端的任务是将抽象对象和实现对象连接起来。
伪代码
此例说明了桥接模式如何帮助划分管理设备及其远程控制的应用程序的单片代码。Device
类充当实现,而 Remote
充当抽象。
基础的远程控制类声明一个引用字段,该字段将它与设备对象链接。所有遥控器(Remote)通过通用设备接口与设备一起工作,这使得同一个遥控器可以支持多种设备类型。
你可以独立于设备类来开发远程控制类。所需要的只是创建一个新的 remote 子类。例如,一个基本的遥控器(remote)可能只有两个按钮,但你可以用额外的功能来扩展它,比如额外的电池或触摸屏。
客户端代码通过遥控器的构造函数将所需类型的遥控器与特定的设备对象链接起来。
// The "abstraction" defines the interface for the "control"
// part of the two class hierarchies. It maintains a reference
// to an object of the "implementation" hierarchy and delegates
// all of the real work to this object.
class RemoteControl is
protected field device: Device
constructor RemoteControl(device: Device) is
this.device = device
method togglePower() is
if (device.isEnabled()) then
device.disable()
else
device.enable()
method volumeDown() is
device.setVolume(device.getVolume() - 10)
method volumeUp() is
device.setVolume(device.getVolume() + 10)
method channelDown() is
device.setChannel(device.getChannel() - 1)
method channelUp() is
device.setChannel(device.getChannel() + 1)
// You can extend classes from the abstraction hierarchy
// independently from device classes.
class AdvancedRemoteControl extends RemoteControl is
method mute() is
device.setVolume(0)
// The "implementation" interface declares methods common to all
// concrete implementation classes. It doesn't have to match the
// abstraction's interface. In fact, the two interfaces can be
// entirely different. Typically the implementation interface
// provides only primitive operations, while the abstraction
// defines higher-level operations based on those primitives.
interface Device is
method isEnabled()
method enable()
method disable()
method getVolume()
method setVolume(percent)
method getChannel()
method setChannel(channel)
// All devices follow the same interface.
class Tv implements Device is
// ...
class Radio implements Device is
// ...
// Somewhere in client code.
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()
radio = new Radio()
remote = new AdvancedRemoteControl(radio)
适用
当要划分和组织一个具有某些功能的多个变体的整体类时,可以使用 Bridge 模式(例如,如果该类可以与各种数据库服务器一起工作)。
一个类越大,就越难弄清楚它是如何工作的,做出改变所需的时间也就越长。对其中一种功能变体所做的更改可能需要在整个类中进行更改,这通常会导致出现错误或无法解决一些关键的副作用。
桥接模式允许你将单一整体类分成多个类层次。之后,你可以独立于其他层次结构中的类来更改每个层次结构的类。这种方法简化了代码维护,并将破坏现有代码的风险降至最低。
当需要在多个正交(独立)维度上扩展类时,可以使用该模式。
Bridge 模式建议你为每个维度提取一个单独的类层次结构。原始类将相关工作委托给属于这些层次结构的对象,而不是自己完成所有工作。
如果需要在运行时切换实现,可以使用桥接模式。
虽然 Bridge 模式是可选的,但它允许你在抽象中替换实现对象。这就像为字段赋值一个新值一样简单。
顺便说一句,最后一项也是为什么这么多人将 Bridge 与 Strategy 模式混淆的主要原因。请记住,模式不仅仅是构建类的特定方式。它还可以传达意图和正在解决的问题。
如何实现
- 确定类中的正交维度。这些独立的概念可以是:抽象/平台,域/基础结构、前端/后端或接口/实现。
- 查看客户端需要什么操作,并在基础抽象类中定义它们。
- 确定所有平台上可用的操作。在通用实现接口中声明抽象需要的那些。
- 对于域中的所有平台,创建具体的实现类,但要确保它们都遵循实现接口。
- 在抽象类内部,为实现类型添加一个引用字段。抽象将大部分工作委托给该字段中引用的实现对象。
- 如果您有多个高级逻辑变体,请通过扩展基本抽象类为每个变体创建精细抽象。
- 客户端代码应该将一个实现对象传递给抽象的构造函数,以便将一个对象与另一个对象相关联。之后,客户端可以忘记实现,只使用抽象对象。
优缺点
- ✔️可以创建独立于平台的类和应用程序。
- ✔️客户端代码使用高级抽象。它没有暴露给平台细节。
- ✔️开闭原则。您可以相互独立地引入新的抽象和实现。
- ✔️单一职责原则。可以在抽象中关注高级逻辑,在实现中关注平台细节。
- ❌通过将模式应用于高度内聚的类,您可能会使代码变得更加复杂。
与其他模式的关系
- 桥接通常是预先设计的,允许您独立地开发应用程序的各个部分。而 Adapter 通常与现有的应用程序一起使用,以使一些不兼容的类能够很好地协同工作。
- 桥接模式、状态模式、策略模式(以及在某种程度上的适配器模式)具有非常相似的结构。事实上,所有这些模式都是基于组合的,即将工作委派给其他对象。然而,它们都能解决不同的问题。模式不仅仅是以特定方式构建代码的配方。它还可以与其他开发人员交流模式所解决的问题。
- 你可以将抽象工厂(Abstract Factory)与桥接模式一起使用。当 Bridge 定义的一些抽象只能用于特定的实现时,这种配对非常有用。在这种情况下,抽象工厂可以封装这些关系,并对客户端代码隐藏复杂性。
- 您可以将 Builder 模式和桥接模式结合起来:director 类扮演抽象的角色,而不同的 builder 充当实现。
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\Bridge\Conceptual;
/**
* The Abstraction defines the interface for the "control" part of the two class
* hierarchies. It maintains a reference to an object of the Implementation
* hierarchy and delegates all of the real work to this object.
*/
class Abstraction
{
/**
* @var Implementation
*/
protected $implementation;
public function __construct(Implementation $implementation)
{
$this->implementation = $implementation;
}
public function operation(): string
{
return "Abstraction: Base operation with:\n" .
$this->implementation->operationImplementation();
}
}
/**
* You can extend the Abstraction without changing the Implementation classes.
*/
class ExtendedAbstraction extends Abstraction
{
public function operation(): string
{
return "ExtendedAbstraction: Extended operation with:\n" .
$this->implementation->operationImplementation();
}
}
/**
* The Implementation defines the interface for all implementation classes. It
* doesn't have to match the Abstraction's interface. In fact, the two
* interfaces can be entirely different. Typically the Implementation interface
* provides only primitive operations, while the Abstraction defines higher-
* level operations based on those primitives.
*/
interface Implementation
{
public function operationImplementation(): string;
}
/**
* Each Concrete Implementation corresponds to a specific platform and
* implements the Implementation interface using that platform's API.
*/
class ConcreteImplementationA implements Implementation
{
public function operationImplementation(): string
{
return "ConcreteImplementationA: Here's the result on the platform A.\n";
}
}
class ConcreteImplementationB implements Implementation
{
public function operationImplementation(): string
{
return "ConcreteImplementationB: Here's the result on the platform B.\n";
}
}
/**
* Except for the initialization phase, where an Abstraction object gets linked
* with a specific Implementation object, the client code should only depend on
* the Abstraction class. This way the client code can support any abstraction-
* implementation combination.
*/
function clientCode(Abstraction $abstraction)
{
// ...
echo $abstraction->operation();
// ...
}
/**
* The client code should be able to work with any pre-configured abstraction-
* implementation combination.
*/
$implementation = new ConcreteImplementationA();
$abstraction = new Abstraction($implementation);
clientCode($abstraction);
echo "\n";
$implementation = new ConcreteImplementationB();
$abstraction = new ExtendedAbstraction($implementation);
clientCode($abstraction);
Output.txt: 执行结果
Abstraction: Base operation with:
ConcreteImplementationA: Here's the result on the platform A.
ExtendedAbstraction: Extended operation with:
ConcreteImplementationB: Here's the result on the platform B.