Golang 设计模式之适配器模式

文摘   科技   2023-06-30 18:57   北京  

1 适配器模式

本期继续和大家探讨 Golang 设计模式这一主题. 今天,我们来聊一聊设计模式中的"适配器模式".

适配器模式的作用是能够实现两个不兼容或弱兼容接口之间的适配桥接作用,该设计模式中会涉及到如下几个核心角色:

  • • 目标 target:是一类含有指定功能的接口

  • • 使用方 client:需要使用 target 的用户

  • • 被适配的类 adaptee:和目标类 target 功能类似,但不完全吻合

  • • 适配器类:adapter:能够将 adaptee 适配转换成 target 的功能类

下面举个直观点的例子来说,我们作为用户(client)现在手中持有一个两孔的插头,需要匹配的目标是一个两孔的插座(target),但是现状是我们只找到了三孔的插座(adaptee),于是我们通过在三孔插座上插上一个实现三孔转两孔的适配器(adapter),最终实现了两孔插头与三孔插座之间的适配使用.

 

 

2 代码实现

第1章聊完概念,让大家对适配器模式用了一个大致的影响,下面我们结合类图和代码,来和大家一起探讨适配器模式的实践案例,从而进一步加深对该种设计模式的理解.

我把适配器模式分为常规适配模式和 interface 适配模式两种类型,下面逐一探讨.

2.1 常规适配模式

下面给出一个适配器模式相对通用的类图.

在上述类图中,包含如下关键点:

  • • 有一个抽象的 Target interface,具备一种核心功能 Operation

  • • 有一个 Client 作为使用方,需要使用到 Target 的 Operation 功能

  • • 分别定义了两种具体类型 ConcreteTypeA 和 ConcreteTypeB,作为 Target 的实现类,都具备 Target 所要求的 Operation 方法

  • • 有一个 Adaptee 类型,它有 Target 相似但又不完全相同的功能 DifferentOperation

  • • 接下来,Client 为了能够把 Adaptee 当作 Target 使用,引入了一个适配器类 Adapter

  • • 在 Adapter 中实现了 Target 所要求的 Operation 方法,同时 Adapter 中引入了 Adaptee 作为成员属性. 在 Adapter 的 Operation 方法中,会调用 Adaptee 的 DifferentOperation 方法,并完成将其适配转换成 Operation 的职责

 

下面我们给出一个具体的场景,进一步加深印象:

 

  • • 市场上手机充电器的通用电压通常设置为5V. 于是我们声明一个 PhoneCharger interface,作为手机充电器的抽象定义,其中包含了一个方法 Output5V,可以保证在充电期间持续稳定地输出 5V 电压

  • • 有关手机充电器的具体实现,根据手机型号,可以分为华为手机充电器 HuaWeiCharger 和小米手机充电器 XiaoMiCharger,两个实现类都实现了 Ouput5V 方法,具备稳定输出 5V 电压的能力

  • • 接下来我们还有一个苹果笔记本的充电器 MacBookCharger,由于适配的是笔记本电脑,因此只有一个 Output28V 方法,对应输出的电压幅值为 28V

  • • 手机充电器的使用方自然是手机. 我们声明一个 Phone interface,代表手机的类型. Phone interface 中包含一个方法 Charge,需要使用到 PhongCharger 的 Output5V 方法,完成对手机的充电

  • • 其中,Phone interface 我们给出一个具体的实现类——华为手机 HuaWeiPhone

  • • 现在存在的一个情况是,华为手机充电器 HuaWeiCharger 和小米手机充电器 XiaoMiCharger 都被占用了,我们希望临时使用苹果笔记本充电器 MacBookCharger 来为手机充电

  • • 由于 MacBookCharger 输出电压为 28V,所以需要引入一个充电适配器 MacBookChargerAdapter 来将 28V 电压转换为 5V,避免在充电过程中对手机造成损坏

理完了这个场景以及对应的类图关系,下面我们 show 一下代码:

其中手机充电器对应为 PhoneCharger interface,存在的核心方法为 Output5V:

type PhoneCharger interface {
    Output5V()
}

 

具体的手机充电器型号包括华为手机充电器和小米手机充电器:

type HuaWeiCharger struct {
}


func NewHuaWeiCharger() *HuaWeiCharger {
    return &HuaWeiCharger{}
}


func (h *HuaWeiCharger) Output5V() {
    fmt.Println("华为手机充电器输出 5V 电压...")
}


type XiaoMiCharger struct {
}


func NewXiaoMiCharger() *XiaoMiCharger {
    return &XiaoMiCharger{}
}


func (x *XiaoMiCharger) Output5V() {
    fmt.Println("小米手机充电器输出 5V 电压...")
}

 

另外还有一种苹果笔记本充电器,输出的电压是 28V:

type MacBookCharger struct {
}


func NewMacBookCharger() *MacBookCharger {
    return &MacBookCharger{}
}


func (m *MacBookCharger) Output28V() {
    fmt.Println("苹果笔记本充电器输出 28V 电压...")
}

 

苹果笔记本输出的电压是 28V,这个手机无法直接承受,因此我们创建出一个手机充电器的适配器类,在适配器类的 Output5V 方法中,会调用苹果笔记本输出电压的能力,并将其适配转换成 5V 输出:

type MacBookChargerAdapter struct {
    core *MacBookCharger
}


func NewMacBookChargerAdapter(m *MacBookCharger) *MacBookChargerAdapter {
    return &MacBookChargerAdapter{
        core: m,
    }
}


func (m *MacBookChargerAdapter) Output5V() {
    m.core.Output28V()
    fmt.Println("适配器将输出电压调整为 5V...")
}

 

定义完手机充电器之后,下面进行手机的类型声明,包括一个 Phone interface 以及一种具体的手机实现类 HuaWeiPhone. Phone 具备一个用于充电的 Charge 方法,其中使用到手机充电器 PhoneCharger 的 Output5V 方法,执行手机的充电操作.

type Phone interface {
    Charge(phoneCharger PhoneCharger)
}


type HuaWeiPhone struct {
}


func NewHuaWeiPhone() Phone {
    return &HuaWeiPhone{}
}


func (h *HuaWeiPhone) Charge(phoneCharger PhoneCharger) {
    fmt.Println("华为手机准备开始充电...")
    phoneCharger.Output5V()
}

 

下面是给出这一场景下的使用示例,首先是对华为手机充电器的使用,然后使用经由适配器转换后的苹果笔记本充电器进行充电:

func Test_adapter(t *testing.T) {
    // 创建一个华为手机实例
    huaWeiPhone := NewHuaWeiPhone()


    // 使用华为手机充电器进行充电
    HuaWeiCharger := NewHuaWeiCharger()
    huaWeiPhone.Charge(HuaWeiCharger)


    // 使用适配器转换后的 macbook 充电器进行充电
    macBookCharger := NewMacBookCharger()
    macBookChargerAdapter := NewMacBookChargerAdapter(macBookCharger)
    huaWeiPhone.Charge(macBookChargerAdapter)
}

上述测试代码对应的输出结果如下:

华为手机准备开始充电...
华为手机充电器输出 5V 电压...


华为手机准备开始充电...
苹果笔记本充电器输出 28V 电压...
适配器将输出电压调整为 5V...

 

2.2 interface 适配模式

在 golang 中,class 对 interface 的实现采用的隐式实现的方式,即我们在定义具体类型时,不需要显式声明对 interface 的 implement 操作,只需要实现了 interface 的所有方法,就自动会被编译器识别为 interface 的一种实现.

下面给出对应的示例:

type MyInterface interface {
    MethodA()
    MethodB()
}


type MyClass struct{}


func NewMyClass() *MyClass {
    return &MyClass{}
}


// MyClass 实现了 MyInterface 声明的所有方法
func (m *MyClass) MethodA() {}


func (m *MyClass) MethodB() {}


func Test_implement(t *testing.T) {
    // 获取 myClass 的类型
    myClassTyp := reflect.TypeOf(NewMyClass())
    // 获取 myInterface 的类型
    myInterTyp := reflect.TypeOf((*MyInterface)(nil)).Elem()
    // 判断是否具有实现关系
    t.Log(myClassTyp.Implements(myInterTyp))
}

对应的输出结果为:

    true

 

正是基于 golang 中这种隐式实现的特性,使得 interface 本身也具备了适配器的功能.

2.1 小节中,我们所探讨的一类常规适配器模式,指的是被适配对象 adaptee 中缺少了一部分目标 target 的核心能力,需要由适配器 adapter 完成这部分能力的适配补齐.

而在接下来的 2.2 小节中,我们聊的是另一种场景:adaptee 不仅具备 target 的全部能力,还聚合了一部分 target 本身不关心的能力. 因此倘若我们直接把 adaptee 当作 target 使用,这部分不相干能力也会被暴露出来,最终对 target 的使用方造成困惑.

这对于这种问题,我们可以通过对 interface 的合适定义使其充当适配器的角色,来规避这类因边界不清晰导致功能泄漏的代码规范问题.

 

下面我们对于 golang 中 interface 的使用进行一轮梳理:

2.2.1 interface 建立接口规范

首先,interface 最常用的一种使用模式,是抽象出了同一类型下多种角色的共性,将其声明成一个接口规范的形式,最终所有实现类 class 都需要实现 interface 的所有方法,或者反过来说,具体 class 本身对应于 interface 类型中的一种具体角色,它就理所应当具备 interface 所抽象出来的核心能力,否则,就只能说明 interface 的抽象程度并不合理.

举个具体的例子,我们定义出了手机的 interface——phone,其中抽象出每种手机都需要具备的能力,包括拨打电话的 Call 方法、充电 Charge 方法、发送短信 SendMessage 方法等...

我们基于品牌维度声明出 Phone 的具体实现类,包括华为手机 HuaWeiPhone、小米手机 XiaoMiPhone、OPPO 手机 OPPOPhone、VIVO 手机 VIVOPhone 等,每种实现类 class 都需要实现好 Phone interface 中定义好的 Call、Charge、SendMessage 等方法.

这个场景对应的类图结构如下:

 

2.2.2 通过 interface 隐藏实现细节

另一种 interface 的使用场景,在模块间进行类的传输时,为了保护具体的实现类隐藏其中的实现细节,转而使用抽象 interface 的形式进行传递. 同时这种基于 interface 进行传递参数的方式,也基于了使用方一定的灵活度,可以通过注入 interface 不同实现 class 的方式,赋予方法更高的灵活度. 这正是我们在编程设计模式中所推崇的面向接口编程而非面向实现编程的思路体现.

 

首先我们给出具体的实现案例:我们需要设计一个课程服务模块,其中包含了一系列课程的学习,包括编程课程、体育课程、音乐课程等等,涉猎的范围非常广泛,能够做到让使用方结合自身的兴趣找到合适的课程资源.

在实现时,我们预定好一个 CourseService interface,其中声明了一系列课程对应的方法;在此之上,我们定义了一个实现 class——courseServiceImpl,统一实现出上述的所有课程方法.

对应的类图以及实现代码如下所示:

 

type CourseService interface {
    // 一系列编程课程
    LearnGolang()
    LearnJAVA()
    LearnC()
    // ...


    // 一系列体育课程
    LearnBasketball()
    LearnFootball()
    LearnSki()
    // ...


    // 一系列音乐课程
    LearnPiano()
    LearnHarmonica()
    LearnGuita()
    // ...
}


type courseServiceImpl struct {
}


func NewCourseService() CourseService {
    return &courseServiceImpl{}
}


func (c *courseServiceImpl) LearnGolang() {
    fmt.Println("learn go...")
}


func (c *courseServiceImpl) LearnJAVA() {
    fmt.Println("learn java...")
}


func (c *courseServiceImpl) LearnC() {
    fmt.Println("learn c...")
}


func (c *courseServiceImpl) LearnBasketball() {
    fmt.Println("learn basketball...")
}


func (c *courseServiceImpl) LearnFootball() {
    fmt.Println("learn football...")
}


func (c *courseServiceImpl) LearnSki() {
    fmt.Println("learn ski...")
}


func (c *courseServiceImpl) LearnPiano() {
    fmt.Println("learn piano...")
}


func (c *courseServiceImpl) LearnHarmonica() {
    fmt.Println("learn harmonica...")
}


func (c *courseServiceImpl) LearnGuita() {
    fmt.Println("learn guita...")
}

 

上面这种实现方式中,interface 和实现 class 是紧密绑定的,遵循还是类似于 JAVA 中显式实现的代码风格,然而这种风格在 golang 中是并不值得推崇的,根本原因就在于 Golang 隐式实现与 JAVA 显式实现的差异.

这种实现方式的缺陷在于:

  • • 对于模块的实现方来说,每次新增或者修改方法,需要同步变更两处内容,包括 interface 和具体的 serviceImpl,增加了变更成本

  • • 对于模块的使用方来说,由于 interface 已经由实现方定好了,这种抽象程度对于使用方来说可能并不合适,导致可能有使用方不关心的方法也被 interface 暴露出来. 这样一方面会对使用方造成使用上的困扰,另一方面也会增加使用方在对 interface 添加更多实现类时的实现成本,因为实现时会涉及到对使用方所不关心的一部分抽象方法的实现

产生这个问题的根本原因在于,构造 interface 的工作不应该由模块的实现方来做. 定义 interface 本质上是一个对类型的边界和职责进行抽象的过程,作为实现方的角色,它永远无法做到未卜先知地站在未来使用方的视角,来帮助使用方做出”如何使用这个模块“的定义和决策.

就举上面给出的这个 CourseService 的示例而言,课程服务的使用方有可能只是使用 CourseService 进行编程课程的学习,那么此时 CourseService 中有关于音乐和体育部分的课程内容对于使用方来说就是无关甚至累赘的一部分信息. 再进一步举个例子,使用方有可能只希望通过 CourseService 进行 Golang 课程的学习,那么此时编程课程中的 JAVA 课程和 C 课程对于使用方来说也属于是无须关心的一部分信息.

为了解决这个实现方与使用方之间视角冲突的问题,最终 Golang 官方给出了对应的解决方案:interface 的定义应该由模块的使用方而非实现方来进行定义. 只有这样,使用方才能根据自己的视角,对模块进行最合适或者说最贴合自己使用需求的抽象定义.

关于这一点内容,可以参见 Golang 官方在 CodeReview 评论中给出一部分有关于 interface 用法的介绍内容:

内容链接:http://github.com/golang/go/wiki/CodeReviewComments#interface

 

看到这里,大家脑海中可能有一些朦胧的概念,但是认知可能还是不够清晰. 我们就趁热打铁,下面就结合适配器模式的主题,针对 CourseService 的场景给出 Golang 所推崇的 interface 的设计架构,以此来展示 interface 所其到的适配器功能.

 

针对于 CourseService 的场景问题,我们进行如下改造:

  • • 实现方不再进行 interface 的声明,而是将 CourseService 改为一个具体的实现类型,将定义 interface 的职责转交给 CourseService 的使用方

  • • 使用方在使用 CourseService 时,根据使用到的 CourseService 的功能范围,对其身份进行抽象和定义,比如在使用 CourseService 中编程课程有关的方法时,使用方可以定义出一个 CSCourseProxy 的 interface,然后在 interface 中定义好有关编程课程的几个方法,其他无关的方法不再声明,起到屏蔽无关职责的效果

 

这样实现下来,对应的类图结构如下所示:

 

接下来我们做一下代码展示:

在实现方这一侧,直接将 CourseService 定义为具体的类型,不再额外通过 interface 进行包装:

type CourseService struct {
}


func NewCourseService() *CourseService {
    return &CourseService{}
}


func (c *CourseService) LearnGolang() {
    fmt.Println("learn go...")
}


func (c *CourseService) LearnJAVA() {
    fmt.Println("learn java...")
}


func (c *CourseService) LearnC() {
    fmt.Println("learn c...")
}


func (c *CourseService) LearnBasketball() {
    fmt.Println("learn basketball...")
}


func (c *CourseService) LearnFootball() {
    fmt.Println("learn football...")
}


func (c *CourseService) LearnSki() {
    fmt.Println("learn ski...")
}


func (c *CourseService) LearnPiano() {
    fmt.Println("learn piano...")
}


func (c *CourseService) LearnHarmonica() {
    fmt.Println("learn harmonica...")
}


func (c *CourseService) LearnGuita() {
    fmt.Println("learn guita...")
}

 

对于使用方来说,需要根据自身对于 CourseService 的适用范围,完成 interface 的定义. 这个定义过程即明确了自身对于 CourseService 职责定位,也实现了对无关方法的消音屏蔽.

在这个过程中,interface 就扮演了适配器的角色,对 class 的范围起到适配和收敛的作为,使其在使用方手中,有一个更加恰到好处的定位和空间.

下面是对 interface 的定义,这里仅仅给出了编程课程、体育课程和音乐课程的三类定义,在实际使用场景中,对于 interface 的粒度还可以有更加灵活自由的控制,完全取决于使用方的使用诉求.

type CSCourseProxy interface {
    LearnGolang()
    LearnJAVA()
    LearnC()
}


type PECourseProxy interface {
    LearnBasketball()
    LearnFootball()
    LearnSki()
}


type MusicCourseProxy interface {
    LearnPiano()
    LearnHarmonica()
    LearnSki()
}

 

比如,使用方如果只需要使用到 CourseService 中有关于 Golang 课程的资源,则可以进一步细化,将 interface 定义为 GolangCourseProxy 的维度:

type GolangCourseProxy interface{
    LearnGolang()
}

 

于是,这个使用方在使用过程中,可以通过 GolangCourseProxy 类型对 CourseService 实例进行介绍,这样该实例的职责是非常单一且清晰的.

func Test_golangCourseProxy(t *testing.T) {
    var proxy GolangCourseProxy = NewCourseService()
    proxy.LearnGolang()
}

 

再比如,使用方倘若需要针对 interface 进行 mock 打桩,那么在做 mock 实现类声明时,成本也是很低的,只需要实现自身聚焦的 LearnGolang 这一个方法即可.

type mockGolangCourseProxy struct {
}


func NewMockGolangCourseProxy() *mockGolangCourseProxy {
    return &mockGolangCourseProxy{}
}


func (m *mockGolangCourseProxy) LearnGolang() {
    fmt.Println("mock learn go...")
}


func Test_mockGolangCourseProxy(t *testing.T) {
    var mockProxy GolangCourseProxy = NewMockGolangCourseProxy()
    mockProxy.LearnGolang()
}

有关 Golang interface 这部分内容,大家如果觉得看完本文理解认知还不够清晰,可以观看一下我之前发表在 bililibili 上的相关讲解视频:

https://www.bilibili.com/video/BV14M411H7J9/?p=7

golang单测心得分享——小徐先生1212

3 总结

本期和大家探讨了设计模式中的适配器模式. 适配器模式的作用是能够实现两个不兼容或者弱兼容接口之间的适配桥接作用,按照我的个人理解可以细分为常规适配模式和 interface 适配模式两种类型,本文中结合概念介绍、类图和代码展示的方式向大家一一作了展示.


小徐先生的编程世界
在学钢琴,主业码农
 最新文章