编程

设计模式之适配器(Adapter)模式

561 2024-01-27 17:42:00

又名 Wrapper

意图

Adapter 是一种结构型设计模式,他允许有着不兼容接口的对象进行合作。

问题描述

假设你创建了一个股票市场监测应用。该应用以 XML 格式从多个源下载股票数据,然后为用户显示了优美的图表。

在某个时刻,你决定整合一个智能的第三方分析库,改进应用。但有一个问题:分析库只适用于 JSON 格式的数据。

您可以修改库以使用 XML。但是,这可能会破坏一些依赖于库的现有代码。更糟糕的是,你可能一开始就无法访问库的源代码,这使得这种方法变得不可能。

方案

你可以创建一个适配器(adapter)。这是一个特殊的对象,它转换一个对象的接口,以便另一个对象能够理解它。

适配器包装其中一个对象,以隐藏幕后发生的转换的复杂性。被包装的对象甚至不知道适配器。例如,可以使用将所有数据转换为英制单位(如英尺和英里)的适配器包裹以米和公里为单位的对象。

适配器不仅可以将数据转换为各种格式,还可以帮助具有不同接口的对象进行协作。以下是它的工作原理:

  1. 适配器获取兼容某个现有对象的接口。
  2. 使用接口,现有对象可以安全地调用适配器方法。
  3. 一旦受到调用,适配器将请求传递给第二个对象,不过以第二个对象期待的格式和顺序传递。

有时甚至可以创建一个可以双向转换的适配器。

让我们回到股票应用。要解决不兼容格式的困境,你可以为代码直接使用的分析库的每个类创建一个 XML-到-JSON 的适配器。然后调整代码,使其仅通过这些适配器与库通信。当适配器接收到调用时,它将传入的 XML 数据转换为 JSON 结构,并将调用传递给封装的分析对象的适当方法。

真实世界对比

在你第一次从美国前往欧洲时,当你试图给笔记本电脑充电时,你可能会大吃一惊。不同国家的电源插头和插座标准不同。这就是为什么你的美国插头装不下德国插座的原因。这个问题可以通过使用具有美式插座和欧式插头的电源插头适配器来解决。

结构

对象适配器

此实现使用对象组合原则:适配器实现一个对象的接口并封装另一个对象。它可以用所有流行的编程语言来实现。

  1. 客户端类(Client))是一个包含程序现有业务逻辑的类。
  2. 客户端接口(Client Interface)描述了一个其他类必须遵循的协议,以便能够与客户端代码协作。
  3. 服务端(Service)是一些有用的类(通常来自第三方或者历史遗留)。客户端不能直接使用这些类,因为他有一个不兼容的接口。
  4. 适配器(Adapter)是一个可以与客户端和服务端协助的类:它实现了客户端接口,包装了服务器对象。适配器通过客户端接口接收来自客户端的调用,并将其转换为能够理解的格式的对封装服务对象的调用。

只要它通过客户端接口与适配器一起工作,客户端代码就不会耦合到具体的适配器类。正因为如此,可以在不破坏现有客户端代码的情况下将新类型的适配器引入到程序中。当服务类的接口被更改或替换时,这可能很有用:你只需创建一个新的适配器类,而无需更改客户端代码。

类适配器

此实现使用继承:适配器同时从两个对象继承接口。请注意,这种方法只能在支持多重继承的编程语言中实现,如 C++。

类适配器不需要包装任何对象,因为其基础了来自客户端和服务端的行为。适配发生在重写的方法中。生成的适配器可以用来代替现有的客户端类。

伪代码

这个适配器模式示例基于方桩和圆孔之间的经典冲突。

适配器假装是一个圆形孔塞,其半径等于正方形直径的一半(换句话说,可以容纳正方形孔塞的最小圆的半径)

// 假设您有两个具有兼容接口的类:
// RoundHole and RoundPeg.
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // 返回圆孔的半径

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // 返回圆形孔塞的半径


// 不过有一个不兼容的类: SquarePeg.
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // 返回方形孔塞宽度


// 适配器类让你将方形的孔塞安装到圆孔中。
// 它扩展了 RoundPeg 类,使得适配器对象
// 圆形孔塞
class SquarePegAdapter extends RoundPeg is
    // 实际上,适配器包含 SquarePeg 类的一个实例。
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // 适配器伪装成圆形孔塞,
        // 其半径可用与适配器实际包裹的方形孔塞相匹配
        return peg.getWidth() * Math.sqrt(2) / 2


// 客户端代码
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 这个不兼容(不可兼容类型)

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

适用

当要使用现有的类,但其接口不兼容其他代码时,可以使用适配器模式。

适配器(Adapter)模式允许你创建一个中间层类,作为代码与遗留类、第三方类或任何其他具有奇怪接口的类之间的转换器。

当要重用多个缺少共同功能、无法添加到超类的子类时,可以使用适配器模式。

你可以扩展每个子类,并将缺失的功能放入新的子类中。然而,你需要在所有这些新类中复制代码,这听起来真的非常糟糕。

更优雅的解决方案是将缺失的功能放入适配器类中。然后,在适配器中封装具有缺失功能的对象,从而动态地获得所需的功能。为了实现这一点,目标类必须有一个公共接口,适配器的字段应该在该接口之后。这种方法看起来与装饰器(Decorator)模式非常相似。

如何实现

  1. 请确保至少有两个不兼容的接口的类:
    1. 一个无法修改的服务端(service)类(通常是第三方类,或者有大量已有依赖的历史遗留的类)。
    2. 一个或多个客户端(client)类可以通过使用服务类获益
  2. 声明客户端接口并描述客户端如何与服务端通信
  3. 创建适配器类使之遵循客户端接口。暂时将所有方法留空。
  4. 添加一个字段到适配器类,以存储服务端对象引用。通常的做法是通过构造函数初始化此字段,但有时在调用该方法时将其传递给适配器会更方便。
  5. 逐个实现适配器类中客户端接口的所有方法。适配器应该将大部分实际工作委托给服务对象,只处理接口或数据格式转换。
  6. 客户端应通过客户端接口使用适配器。这将允许你在不影响客户端代码的情况下更改或扩展适配器。

优缺点

  • ✔️单一职责原则。你可以从主要的业务逻辑中分离接口或数据转换代码。
  • ✔️开闭原则。你可以在不破坏下游客户端代码的情况下将新类型的适配器引入到程序中,只要它们能够通过客户端接口与适配器协作。
  • ❌代码的总体复杂性会增加,因为你需要引入一组新的接口和类。有时,只需更改服务类以使其与代码的其余部分匹配会更简单。

与其他模式的关系 

桥接模式(Bridge)通常是预先设计的,允许你独立地开发应用程序的各个部分。而适配器通常与现有的应用程序一起使用,以使一些不兼容的类能够很好地协同工作。

适配器模式(Adapter)为访问现有对象提供了一个完全不同的接口。而使用装饰器(Decorator)模式,接口要么保持不变,要么进行扩展。此外,Decorator 支持递归组合,这在使用 Adapter 时是不可能的。

使用适配器模式(Adapter),可以通过不同的接口访问现有对象。使用代理模式(Proxy)时,界面保持不变。使用装饰器(Decorator)模式,可以通过增强的界面访问对象。

门面模式(Facade)为已有的对象定义一个新的接口,而适配器尝试使现有接口可用。适配器通常只封装一个对象,而 Facade 则处理整个对象子系统。

桥接模式(Bridge)、状态模式(State)、策略模式(Strategy) (以及某种某种程度上的适配器模式Adapter)结构非常相似。确实,这些模式都是基于组合,委派任务给其他对象。不过,它们解释的是不通过的问题。模式不仅仅是以特定方式构建代码的配方。它还可以与其他开发人员交流模式所解决的问题。

代码示例

index.php: 概念示例

<?php

namespace RefactoringGuru\Adapter\Conceptual;

/**
 * The Target defines the domain-specific interface used by the client code.
 */
class Target
{
    public function request(): string
    {
        return "Target: The default target's behavior.";
    }
}

/**
 * The Adaptee contains some useful behavior, but its interface is incompatible
 * with the existing client code. The Adaptee needs some adaptation before the
 * client code can use it.
 */
class Adaptee
{
    public function specificRequest(): string
    {
        return ".eetpadA eht fo roivaheb laicepS";
    }
}

/**
 * The Adapter makes the Adaptee's interface compatible with the Target's
 * interface.
 */
class Adapter extends Target
{
    private $adaptee;

    public function __construct(Adaptee $adaptee)
    {
        $this->adaptee = $adaptee;
    }

    public function request(): string
    {
        return "Adapter: (TRANSLATED) " . strrev($this->adaptee->specificRequest());
    }
}

/**
 * The client code supports all classes that follow the Target interface.
 */
function clientCode(Target $target)
{
    echo $target->request();
}

echo "Client: I can work just fine with the Target objects:\n";
$target = new Target();
clientCode($target);
echo "\n\n";

$adaptee = new Adaptee();
echo "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
echo "Adaptee: " . $adaptee->specificRequest();
echo "\n\n";

echo "Client: But I can work with it via the Adapter:\n";
$adapter = new Adapter($adaptee);
clientCode($adapter);

Output.txt: 执行结果

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.