适配器

适配器模式有两种。在可能的情况下,我更倾向于使用对象适配器,而不是类适配器;稍后我会对此进行详细解释。

适配器模式允许现有类与不匹配的接口一起使用。它通常用于允许现有类与其他类协同工作,而无需修改其源代码。

这在使用第三方库(每个库都有自己的接口)的多态环境中非常有用。

从根本上说,适配器可以帮助两个不兼容的接口协同工作。通过将一个类的接口转换为客户端所期望的接口,可以使原本不兼容的类协同工作。

类适配器

在类适配器中,我们使用继承来创建适配器。一个类(适配器)可以继承另一个类(被适配者);使用标准继承,我们可以为被适配者添加额外的功能。

假设我们在 ATM.php 文件中有一个 ATM 类:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$Chapter 4/ClassAdapter/ATM.php[]

让我们创建 ATMWithPhoneTopUp.php 来形成我们的适配器:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$/Chapter 4/ClassAdapter/ATMWithPhoneTopUp.php[]

让我们将它们全部包装在一个 index.php 文件中:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$Chapter 4/ClassAdapter/index.php[]

现在我们已经调整了最初的 ATM 类来生成充值代码,现在我们可以利用这个新的充值功能。所有这些的输出如下:

450
Top-up code: 5014606939121598
450

需要注意的是,如果我们想适应多重适配器,这在 PHP 中是很困难的。

在 PHP 中,多重继承是不可能的,除非你使用的是 Traits。在这种情况下,我们只能调整一个类来匹配另一个类的接口。

我们不使用这种方法的另一个关键架构原因是,通常良好的设计更倾向于组合而非继承(如复合重用原则所述)。

为了更详细地探讨这一原则,我们需要了解一下对象适配器。

对象适配器

复合重用原则(Composite Reuse Principle)指出,类应通过其组成实现多态行为和代码重用。根据这一原则,当类要实现某项功能时,应包含其他类的实例,而不是从基类或父类继承功能。为此,"四人帮" 发表了如下声明:

赞成对象组合,反对类继承。

为什么这一原则如此重要?请看上一个例子,我们使用了类继承;在这种情况下,无法从形式上保证我们的适配器与我们想要的接口相匹配。如果父类暴露了我们不希望适配器使用的函数呢?组合为我们提供了更多控制。

通过使用组合而非继承,我们可以更好地支持多态行为,这在面向对象编程中至关重要。

假设我们有一个生成保险费的类。它提供了月保费和年保费,这取决于客户希望如何支付保费。如果按年支付,客户可以节省相当于半个月的保费:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$/Chapter 4/ObjectAdapter/Insurance.php[]

假设一个市场比较工具多态地使用类(如前面提到的类)来计算来自多个不同供应商的保险报价;他们使用这个接口来完成这项工作:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$/Chapter 4/ObjectAdapter/MarketCompare.php[]

因此,我们可以使用这个接口来构建一个对象适配器,以确保我们的保险类(即保费生成器)与市场比较工具所期望的接口相匹配:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$/Chapter 4/ObjectAdapter/InsuranceMarketCompare.php[]

请注意,该类实际上是在为它要适配的内容实例化自己的类。

然后,适配器会将该类存储在一个私有变量中。然后,我们使用私有变量中的这个对象来代理请求。

适配器,无论是类适配器还是对象适配器,都应该起到胶水代码的作用。我的意思是,适配器不应该执行任何计算或运算,而只是充当不兼容接口之间的代理。

我们的标准做法是不在胶水代码中加入逻辑,而是将逻辑留给我们要适配的代码。如果这样做违背了 "单一责任原则"(Single Responsibility Principle),我们就需要改编另一个类。

正如我前面提到的,在类适配器中无法真正实现对多个类的适配,因此,要么将此类逻辑封装在 Trait 中,要么我们就需要使用对象适配器,比如我们在这里讨论的对象适配器。

让我们试试这个适配器。我们将编写下面的 index.php 文件,看看我们的新类是否符合预期的接口:

Unresolved include directive in modules/ROOT/pages/ch04/ch4-03.adoc - include::example$Chapter 4/ObjectAdapter/index.php[]

输出应如下所示:

48.75
48.75
560.625

与类适配器方法相比,该方法的主要缺点是我们必须实现通用方法,即使这些方法仅仅是转发方法。