软件设计模式-适配器模式

三味码屋 2022年07月02日 1,129次浏览

什么是适配器模式

概念

  适配器模式(Adapter Pattern),是一种使用中间层(适配器)来关联新接口与旧接口的设计模式,可以在不改变现有代码的基础上,对现有代码进行拓展,以满足新接口的需要。

例子

  其实,在我们的日常生活中有很多关于适配器的例子,最常见的就是手机充电器。家庭用电一般是交流电,且电压比较高(大于100伏),而手机等小型电子设备一般用的是直流电,且电压比较低(几伏或者十几伏),如果直接使用家庭用电给手机充电,极有可能对手机造成损坏,所以手机一般都会配有一个电源插头和一条USB线,这个电源插头就是电源适配器,也就是我们常说的充电器,电源适配器中包含变压器和整流器,其中变压器可以改变输入电流的电压大小,整流器可以把交流电变为直流电,高压交流电经过电源适配器处理之后就变成了低压直流电,电源适配器把处理后的电流通过USB线输送至手机端,手机就能正常充电了。
  在手机电源适配器的例子中,我们的需求是给手机充电,但是目前的情况是家庭用电为高压交流电,为了解决电流不匹配的问题,我们找来了电源适配器,电源适配器通过变压、整流操作将高压交流电变为了低压直流电,从而实现了给手机充电的目标。对应到软件设计模式中来,其实是类似的,适配器模式通过创建适配器,对现有代码进行了拓展,使得拓展后的代码能够达到目标需求的需要,解决了日益增长的需求和现有情况不能满足需求之间的矛盾。
  根据例子,我们可以发现,适配器模式中包含了3个元素,即:

  1. 需求(目标接口);
  2. 现状(现状不能满足需求,需要被适配);
  3. 适配器(对现状进行拓展,以满足需求)。

  接下来,我们将围绕这3个元素具体讲解适配器模式。

适配器模式分类

  根据实现方式的不同,适配器模式可分为两种不同的类型:

  1. 类适配器模式(通过继承的方式实现);
  2. 对象适配器模式(通过委托的方式实现)。

  接下来,我们具体讲解一下这两种不同的适配器模式。
  在讲解之前,我们需要明确在适配器模式中有哪些接口和类,在上面手机电源适配器的例子中,我们总结出了适配器模式包含3个元素,即:需求、现状、适配器,那么我们可以将这3个元素提取出来作为适配器模式中的接口或类。

  • 需求可以看作还未实现的目标,是抽象的,因此我们用接口来描述需求,接口名定为ITarget
  • 现状可以认为是已实现的功能,是具象的,因此我们用类来描述现状,同时,现状要想满足需求,还需要被适配,所以我们类名定为Adaptee(适配是Adapt,ee可表示动作的接受者,因此Adaptee可表示被适配者)。
  • 适配器会对现状进行适配,也是一个具体的事物,所以我们也用类来表示适配器,类名定为Adapter

类适配器模式

  类适配器模式是指适配器(Adapter)继承现状(Adaptee),并同时实现需求(ITarget),从而将需求的具体实现转移到现状(Adaptee)上来。在类适配器模式中,ITargetAdapteeAdapter各自的代码及相互之间的关系如下:
ITarget

/**
 * 目标接口,表示需要实现的接口
 */
interface ITarget {

    /**
     * 目标接口方法
     */
    fun targetMethod()
}

Adaptee

/**
 * 被适配者,即需要被适配的类
 */
open class Adaptee {

    fun method() {
        println("method invoked")
    }
}

Adapter

/**
 * 适配器
 *
 * 通过继承[Adaptee]并实现[ITarget],使[Adapter]同时拥有[Adaptee]和[ITarget]的能力,
 * 从而将[ITarget]的具体实现转移到[Adaptee]类。
 */
class Adapter : Adaptee(), ITarget {

    override fun targetMethod() {
        // 访问父类[Adaptee]的method()方法
        // 将[ITarget]接口方法的具体实现转移到[Adaptee]类
        method()
    }
}

  在类适配器模式中,Adaptee是抽象类,被Adapter继承,这样在Adapter中便能够直接访问Adaptee暴露出来的方法,在实现ITarget接口方法时,可以通过调用Adaptee的方法来实现具体逻辑。
  类适配器模式UML类图如下:
ClassAdapter.png

对象适配器模式

  对象适配器模式是指适配器(Adapter)持有现状(Adaptee)的对象,并同时实现需求(ITarget),从而将需求的具体实现委托给(Adaptee)对象。在对象适配器模式中,ITargetAdapteeAdapter各自的代码及相互之间的关系如下:
ITarget

/**
 * 目标接口,表示需要实现的接口
 */
interface ITarget {

    /**
     * 目标接口方法
     */
    fun targetMethod()
}

Adaptee

/**
 * 被适配者,即需要被适配的类
 */
class Adaptee {

    fun method() {
        println("method invoked")
    }
}

Adapter

/**
 * 适配器
 *
 * 通过持有[Adaptee]对象并实现[ITarget],使[Adapter]拥有[ITarget]的能力,并能够访问[Adaptee]对象公开出来的方法,
 * 从而将[ITarget]的具体实现委托给[Adaptee]对象。
 */
class Adapter : ITarget {

    private val adaptee: Adaptee by lazy { Adaptee() }

    override fun targetMethod() {
        // 调用[Adaptee]对象的method()方法
        // 将[ITarget]接口方法的具体实现委托给[Adaptee]对象
        adaptee.method()
    }
}

  在对象适配器模式中,Adaptee不是抽象类,Adapter持有Adaptee对象,这样在Adapter中便能够通过Adaptee对象访问Adaptee暴露出来的方法,在实现ITarget接口方法时,可以通过调用Adaptee对象的方法来实现具体逻辑。
  对象适配器模式UML类图如下:
ObjectAdapter.png

示例

  接下来,我们通过“水力发电”的例子来演示两种适配器模式,便于我们更好的理解。
  我们都知道,水不能直接变为电,如果要想通过水产生电,必须通过能量转换装置将水能变为电能,水力发电设备就是这样一种装置,水力发电设备利用水位落差及水的流动产生的势能来驱动水轮转动,水轮转动产生的机械能再驱动发电机发电。
  水力发电的整个过程,也可以看成适配器模式,在这个过程中,需求是“发电机发电”,现状是“只有水和水带来的水能”,水能没法直接变为电能,所以我们必须通过水力发电装置进行能量转换,水力发电装置就是适配器
  我们先来创建示例用到的最基本的角色-“水能”、“电能”、“能量”,水能”、“电能”都蕴含“能量”,所以我们可以定义以下3个类:
Energy

/**
 * 能量
 *
 * @param id 能量唯一标识
 */
data class Energy(val id: String)

HydroEnergy

/**
 * 水能
 *
 * @param energy 能量
 */
data class HydroEnergy(val energy: Energy)

ElectricEnergy

/**
 * 电能
 *
 * @param energy 能量
 */
data class ElectricEnergy(private val energy: Energy)

  接下来,我们分别用类适配器模式和对象适配器模式来编写示例代码。

类适配器模式

  在类适配器模式中,我们把需求抽象为IElectricEnergyDevice接口。IElectricEnergyDevice

/**
 * 电能设备接口
 */
interface IElectricEnergyDevice {

    /**
     * 输出电能
     * @return 电能
     */
    fun outputElectricEnergy(): ElectricEnergy
}

把现状抽象为HydroEnergyDevice类。HydroEnergyDevice

/**
 * 水能设备
 */
open class HydroEnergyDevice {

    /**
     * 提供水能
     */
    fun inputHydroEnergy(): HydroEnergy {
        return HydroEnergy(Energy(hashCode().toString()))
    }
}

适配器则用HydropowerAdapter类来表示。HydropowerAdapter

/**
 * 水力发电适配器,即水力发电装置,将水能转化为电能
 */
class HydropowerAdapter : HydroEnergyDevice(), IElectricEnergyDevice {

    override fun outputElectricEnergy(): ElectricEnergy {
        val hydroEnergy = inputHydroEnergy()
        println("${LABEL}提供水能($hydroEnergy)")
        val electricEnergy = electricEnergyToHydroEnergy(hydroEnergy)
        println("${LABEL}把水能($hydroEnergy)转换成为电能($electricEnergy)")
        println("${LABEL}输出电能($electricEnergy)")
        return electricEnergy
    }

    /**
     * 将水能转化为电能
     */
    private fun electricEnergyToHydroEnergy(hydroEnergy: HydroEnergy): ElectricEnergy {
        return ElectricEnergy(hydroEnergy.energy)
    }

    private companion object {

        private const val LABEL = "【水力发电适配器】"
    }
}

  类适配器模式示例逻辑:

  • 电能设备IElectricEnergyDevice接口需要通过outputElectricEnergy()方法输出电能;
  • 水能设备HydroEnergyDevice只能通过inputHydroEnergy()方法提供水能;
  • 于是水力发电适配器HydropowerAdapter继承了水能设备HydroEnergyDevice,同时实现了电能设备IElectricEnergyDevice接口,水力发电适配器HydropowerAdapter具备了提供水能和输出电能两种能力;
  • 水力发电适配器HydropowerAdapter内部实现了将水能转化为电能的方法electricEnergyToHydroEnergy()
  • 水力发电适配器HydropowerAdapter在实现输出电能的方法outputElectricEnergy()时,先调用inputHydroEnergy()方法获取水能,再调用electricEnergyToHydroEnergy()方法将水能转化为电能,最后输出了电能。

  接下来,我们开始构建类适配器模式场景模拟器ClassAdapterPatternSceneSimulator

/**
 * 类适配器模式场景模拟器
 */
class ClassAdapterPatternSceneSimulator : ISceneSimulator {

    override fun run() {
        println("============类适配器模式示例============")
        val electricEnergyDevice: IElectricEnergyDevice = HydropowerAdapter()
        electricEnergyDevice.outputElectricEnergy()
        println("============类适配器模式示例============")
    }

    companion object {

        /**
         * 运行场景
         */
        fun run() {
            ClassAdapterPatternSceneSimulator().run()
        }
    }
}

  在ClassAdapterPatternSceneSimulator中,使用适配器HydropowerAdapter实例化IElectricEnergyDevice接口对象electricEnergyDevice,通过调用electricEnergyDeviceoutputElectricEnergy方法输出电能。
  最后,我们通过测试代码运行一下类适配器模式场景模拟器,测试代码:

/**
 * 类适配器模式测试类
 */
class Main {

    /**
     * 演示类适配器模式
     */
    @Test
    fun main() {
        ClassAdapterPatternSceneSimulator.run()
    }
}

运行之后,控制台输出如下日志:

============类适配器模式示例============
【水力发电适配器】提供水能(HydroEnergy(energy=Energy(id=1175185734)))
【水力发电适配器】把水能(HydroEnergy(energy=Energy(id=1175185734)))转换成为电能(ElectricEnergy(energy=Energy(id=1175185734)))
【水力发电适配器】输出电能(ElectricEnergy(energy=Energy(id=1175185734)))
============类适配器模式示例============

可以看到,类适配器模式实现了把水能转化为电能的需求。

对象适配器模式

在对象适配器模式中,我们仍然把需求抽象为IElectricEnergyDevice接口。IElectricEnergyDevice

/**
 * 电能设备接口
 */
interface IElectricEnergyDevice {

    /**
     * 输出电能
     * @return 电能
     */
    fun outputElectricEnergy(): ElectricEnergy
}

现状仍然用HydroEnergyDevice类来表示,但不再是抽象类,因为此时现状不需要被适配器继承了。HydroEnergyDevice

/**
 * 水能设备
 */
class HydroEnergyDevice {

    /**
     * 提供水能
     */
    fun inputHydroEnergy(): HydroEnergy {
        return HydroEnergy(Energy(hashCode().toString()))
    }

    companion object {

        const val LABEL = "【水能设备】"
    }
}

适配器仍然用HydropowerAdapter类来表示,HydropowerAdapter类实现了IElectricEnergyDevice接口,但不再继承HydroEnergyDevice,而是在内部持有HydroEnergyDevice对象。HydropowerAdapter

/**
 * 水力发电适配器,即水力发电装置,将水能转化为电能
 */
class HydropowerAdapter : IElectricEnergyDevice {

    private val hydroEnergyDevice: HydroEnergyDevice by lazy {
        println("${LABEL}持有${HydroEnergyDevice.LABEL}")
        HydroEnergyDevice()
    }

    override fun outputElectricEnergy(): ElectricEnergy {
        val hydroEnergy = hydroEnergyDevice.inputHydroEnergy()
        println("${HydroEnergyDevice.LABEL}提供水能($hydroEnergy)")
        val electricEnergy = electricEnergyToHydroEnergy(hydroEnergy)
        println("${LABEL}把水能($hydroEnergy)转换成为电能($electricEnergy)")
        println("${LABEL}输出电能($electricEnergy)")
        return electricEnergy
    }

    /**
     * 将水能转化为电能
     */
    private fun electricEnergyToHydroEnergy(hydroEnergy: HydroEnergy): ElectricEnergy {
        return ElectricEnergy(hydroEnergy.energy)
    }

    private companion object {

        private const val LABEL = "【水力发电适配器】"
    }
}

对象适配器模式示例逻辑:

  • 电能设备IElectricEnergyDevice接口需要通过outputElectricEnergy()方法输出电能;
  • 水能设备HydroEnergyDevice只能通过inputHydroEnergy()方法提供水能;
  • 于是水力发电适配器HydropowerAdapter实现了电能设备IElectricEnergyDevice接口,同时持有了水能设备HydroEnergyDevice对象,水力发电适配器HydropowerAdapter具备了提供水能和输出电能两种能力;
  • 水力发电适配器HydropowerAdapter内部实现了将水能转化为电能的方法electricEnergyToHydroEnergy()
  • 水力发电适配器HydropowerAdapter在实现输出电能的方法outputElectricEnergy()时,先调用HydroEnergyDevice对象的inputHydroEnergy()方法获取水能,再调用electricEnergyToHydroEnergy()方法将水能转化为电能,最后输出了电能。

  接下来,我们开始构建对象适配器模式场景模拟器ObjectAdapterPatternSceneSimulator

/**
 * 对象适配器模式场景模拟器
 */
class ObjectAdapterPatternSceneSimulator : ISceneSimulator {

    override fun run() {
        println("============对象适配器模式示例============")
        val electricEnergyDevice: IElectricEnergyDevice = HydropowerAdapter()
        electricEnergyDevice.outputElectricEnergy()
        println("============对象适配器模式示例============")
    }

    companion object {

        /**
         * 运行场景
         */
        fun run() {
            ObjectAdapterPatternSceneSimulator().run()
        }
    }
}

  在ObjectAdapterPatternSceneSimulator中,使用适配器HydropowerAdapter实例化IElectricEnergyDevice接口对象electricEnergyDevice,通过调用electricEnergyDeviceoutputElectricEnergy方法输出电能。
  最后,我们通过测试代码运行一下对象适配器模式场景模拟器,测试代码:

/**
 * 对象适配器模式测试类
 */
class Main {

    /**
     * 演示对象适配器模式
     */
    @Test
    fun main() {
        ObjectAdapterPatternSceneSimulator.run()
    }
}

运行之后,控制台输出如下日志:

============对象适配器模式示例============
【水力发电适配器】持有【水能设备】
【水能设备】提供水能(HydroEnergy(energy=Energy(id=165245056)))
【水力发电适配器】把水能(HydroEnergy(energy=Energy(id=165245056)))转换成为电能(ElectricEnergy(energy=Energy(id=165245056)))
【水力发电适配器】输出电能(ElectricEnergy(energy=Energy(id=165245056)))
============对象适配器模式示例============

可以看到,通过对象适配器模式也实现了把水能转化为电能的需求。

总结

  在软件迭代过程中,我们会接到各种各样层出不穷的新需求,当新需求和现有功能之间存在关联关系,但现有代码又无法直接用于新需求时,我们可以选择对现有代码进行调整,或者想办法对现有代码进行拓展,如果是选择直接对现有代码进行调整,那么很可能需要对改动的地方及相关联的部分进行大规模回归测试,影响面较大,为了降低测试成本,我们可以选择对现有代码进行拓展,此时适配器模式就派上用场了——不改现有代码,需求轻松拿捏!