首页 > 编程笔记 > Java笔记 阅读:13

Java适配器模式详解(附带实例)

适配器模式也叫作包装器(wrapper)模式,是一种极为重要的模式,也是经典的 GoF 设计模式之一。

适配器是把受适配的类(也就是插在这个适配器上面的那个类)所具备的行为给包裹起来,让用户无须修改该类的行为即可将其当作自己想要的类(也就是目标类)使用。

一般来说,受适配的那个类所具备的接口与用户想要使用的接口是不兼容的,然而适配器模式能够让二者兼容起来,使得用户通过目标接口方便地访问由受适配的类所提供的功能。

适配器模式的主要目标是把某种接口与用户想要使用的另一种接口联系起来,让前者(也就是来源接口)能够当成后者(也就是目标接口)使用。这个模式让那些本来因为扩展的基类或实现的接口不同而无法一起运作的类变得能够相互协作。

在 java.base 模块中有好多地方都用到了适配器模式,其中,java.util 包里名为 Collections 的工具类提供了一个名为 list 的方法,该方法接受一个实现了 Enumeration 接口的对象并返回一个适配之后的 ArrayList,使得来源对象(也就是Enumeration)能够当成目标对象(也就是 ArrayList)使用。

Java适配器模式实例

适配器模式可以用多种方式实现,下面的实例演示了其中一种,我们用这样的方式把各种不同的发动机都适配成 Vehicle 所需要的发动机。

【实例】虽然车辆的某些操作可以直接通过发动机的相关操作来完成,但也有一些操作不是这样,例如,它们的 refuel() 方法,就需要根据发动机的具体类型来决定应该怎么加油。
public static void main(String[] args) {
    System.out.println("Adapter Pattern: engines");
    var electricEngine = new ElectricEngine();
    var enginePetrol = new PetrolEngine();
    var vehicleElectric = new Vehicle(electricEngine);
    var vehiclePetrol = new Vehicle(enginePetrol);

    vehicleElectric.drive();
    vehicleElectric.refuel();
    vehiclePetrol.drive();
    vehiclePetrol.refuel();
}
程序输出结果如下:

Adapter Pattern: engines
...
Vehicle, stop
Vehicle needs recharge
ElectricEngine, check plug
ElectricEngine, recharging
...
Vehicle needs petrol
PetrolEngine, tank

这些发动机之间在功能上有相似之处,但并非完全相同。它们实际上是彼此不同的对象,无法完全按照同一套逻辑来运作,所以必须分别适配,参见下图。


图 1 用UML类图演示发动机功能的不同

在这个例子里,Vehicle 类及其实例扮演了适配器的角色,这种适配器让不同类型的发动机都能够正确地执行车辆所应支持的一套方法。

其中,对于 drive() 这样的方法来说,无论车辆用的是哪种发动机,我们都只需要调用这个发动机的 run() 方法,所以不用对各种发动机分别适配。

但是 refuel() 的方法则不然,为了正确实现这个方法,我们需要调用发动机的 tank() 方法,可是每一种发动机需要用不同的流程执行这个方法,因此我们必须先知道发动机的具体类型,然后才能根据这个类型决定应该采用什么样的流程执行 tank() 方法,参见下面的实例。

【实例】Vehicle 类的 refuel() 方法利用带有模式匹配功能的 switch 结构对各种不同的发动机进行适配,让它们都能正确地支持 refuel() 方法。
class Vehicle {
    private final Engine engine;

    ...

    void refuel() {
        System.out.println("Vehicle, stop");
        switch (engine) {
            case ElectricEngine de -> {
                System.out.println("Vehicle needs diesel");
                de.checkPlug();
                de.tank();
            }
            case PetrolEngine pe -> {
                System.out.println("Vehicle needs petrol");
                pe.tank();
            }
            default -> throw new IllegalStateException("Vehicle has no engine");
        }
    }
}
我们可以利用 Java 语言的一些新特性来简化实现代码,例如,本例就运用了新式的 switch 结构来简化这套分别处理各式发动机的逻辑。

新式的 switch 结构让我们能够在书写每个 case 标签的时候,方便地声明与该标签所要处理的类型相对应的变量,从而在这个 case 分支里操纵该变量,而不像原来那样通过 instanceof 等手段分别判断 engine 参数是否属于某种具体的发动机类型,如果是,再将其转换为那种类型。

另外,我们还用到了一个新的机制,也就是 sealed(密封)。该机制能够明确体现出某个类型(例如,本例的 Engine 接口)只能由指定的类型(例如,本例的 ElectricEngine 与 PetrolEngine)扩展,而不能由无关的类型扩展,这样做使得程序更容易维护。

我们让本例中的两种发动机都实现 Engine 接口,从而令二者具备相同的抽象,但同时我们又禁止其他类型实现该接口,以确保适配器类(也就是 Vehicle 类)不会遇到其他某个类型也实现了 Engine 接口自己却无法适配的尴尬局面(参加下面的实例)。

【实例】让 Engine 接口只能由指定的类型来扩展。
sealed interface Engine permits ElectricEngine, PetrolEngine {
    void run();
    void tank();
}

Vehicle 类要通过适当的逻辑将各种发动机适配成自己需要的类型,也就是支持 drive 与 refuel 的这种。例如,ElectricEngine 发动机除了 Engine 接口规定的 run() 与 tank() 方法,还有一个 checkPlug() 方法,因此 Vehicle 在适配这种发动机的时候,还必须考虑到这个方法(参见下面的实例)。

【实例】ElectricEngine 类型的发动机除了通用的 Engine 接口所规定的方法,还具备自身特有的逻辑。
final class ElectricEngine implements Engine {
    @Override
    public void run() {
        System.out.println("ElectricEngine, run");
    }

    @Override
    public void tank() {
        System.out.println("ElectricEngine, recharging");
    }

    public void checkPlug() {
        System.out.println("ElectricEngine, check plug");
    }
}

总结

适配器模式属于结构型设计模式,它在开发工作中很有意义,这种模式能够以一种易于维护的方式把两种不同的功能联系起来,使得其中一种功能可以融合到另一种功能的接口中。实现适配的这个适配器可以通过适当的封装变得更加抽象。

另外,我们还可以用 Java 语言的 sealed 机制来限定有待适配的类型,使得适配器不用考虑如何适配这一范围之外的类型,这样能够让该模式写起来比较容易,而且能够实现得更加清晰。

决定运用适配器模式意味着,这个适配器本身需要依照受适配者的那套接口来完成适配,而且要考虑到每一种受适配的类所具有的一些特性。

除了采用本节这样的实现方式,你还可以考虑用子类完成适配,这样能够在适配的时候,针对不同的受适配者分别扩充其功能。

如果你要使用第三方的库或 API 来完成自己的功能,但是这两边的接口互不兼容,那么可以考虑用这个模式做适配。这样能把程序代码与第三方代码之间的耦合解开,因为你只需要通过适配器去操纵那些代码就好,而不用在主要的程序代码里直接操纵它们,而且这样还符合 SOLID 原则。用适配器模式做出来的代码重构起来也比较容易。

相关文章