适配器

270 阅读9分钟

适配器

适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时,适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性

插头与插孔的冲突

举一个生活中常见的实例,我们新买了一台电视机,其电源插头是两相的,不巧的是墙上的插孔却是三相的,这时电视机便无法通电使用。我们以代码来重现这个场景,首先得将墙上的三相插孔接口确立下来

package adapter;

public interface TriplePin {
    //火线、零线、地线
    public void electrify(int l,int n,int e);
}

我们为三相插孔接口TriplePin定义了一个三插通电标准electrify(),其中3个参数l、n、e分别对应火线(live)、零线(null)和地线(earth)。同样,我们定义两相插孔接口

package adapter;

public interface DualPin {
    //没有地线
    public void electrify(int l,int n);
}

与三相插孔接口所不同的是,两相插孔接口DualPin定义的是2个参数的通电标准,可以看到electrify()的参数中缺少了地线e。插孔接口定义完毕,接下来可以定义电视机类了。如之前提到的,电视机的两相插头是两插标准,所以它实现的是两相插孔接口DualPin

package adapter;

public class TV implements DualPin{
    @Override
    public void electrify(int l, int n) {
        System.out.println("火线通电: "+l+", 零线通电:"+n);
        System.out.println("电视开机");
    }
}

因为电视机类TV实现了两相插孔接口DualPin,所以代码第4行的通电方法electrify()只接通火线与零线,然后开机。代码很简单,而目前我们面临的问题是,墙上的接口是三相插孔,而电视机实现的是两相插孔,二者无法匹配

package adapter;

public class Client {
    public static void main(String[] args) {
        TriplePin device = new TV();//接口不兼容
    }
}

通用适配

针对接口不兼容的情况,可能有人会提出比较极端的解决方案,就是把插头掰弯强行适配,若是三相插头接两相插孔的话,就把零线插针拔掉。虽然目的达到了,但经过这么一番暴力修改,插头也无法再兼容其原生接口了,这显然是违背设计模式原则的。

为了不破坏现有的电视机插头,我们需要一个适配器来做电源转换,有了它我们便可以顺利地把电视机两相插头转接到墙上的三相插孔中了

package adapter;

public class Adapter implements TriplePin {
    private  DualPin dualPinDevice;

    //创建适配器时,需要把两插设备接入进来
    public Adapter(DualPin dualPinDevice){
        this.dualPinDevice = dualPinDevice;
    }

    //适配器实现的是目标接口
    @Override
    public void electrify(int l, int n, int e) {
        //调用被适配设备的两插通电方法,忽略地线参数e
        dualPinDevice.electrify(l,n);
    }
}

与电视机类不同的是,适配器类Adapter实现的是三相插孔接口,这意味着它能够兼容墙上的三相插孔了。注意代定义的两相插孔的引用,我们在构造方法中对其进行初始化,也就是说,适配器中嵌入一个两相插孔,任何此规格的设备都是可以接入进来的。最后,在三相插孔通电方法中,适配器转去调用了接入的两插设备,并且丢弃了地线参数e,这就完成了三相转两相的调制过程,最终达到适配效果。至此,这个适配器就可以将任意两插设备匹配到三相插孔上了。我们来看如何让电视机接通电源

package adapter;

public class Client {
    public static void main(String[] args) {
//        TriplePin device = new TV();//接口不兼容
        DualPin dualPinDevice = new TV();
        TriplePin triplePinDevice = new Adapter(dualPinDevice);//适配器驳斥两端
        triplePinDevice.electrify(1,0,-1);//此处调用的是三插通电标准
        //输出结果
        //火线通电:1,零线通电:0,
        //电视开机
    }
}

客户端类构造的是两插标准的电视机对象,接着给构造好的适配器注入电视机对象(将电视机两相插头插入适配器),并将其赋给三相插孔接口(将匹配好的适配器插入墙上的三相插孔)。最后,我们直接调用三插通电方法给电视机供电,如第9行的输出结果所示,表面上看我们使用的是三插通电标准,而实际上是用两插标准为电视机供电(只使用了火线与零线),最终电视机顺利开启,两插标准的电视机与三相插孔接口成功得以适配。需要注意的是,适配器并不关心接入的设备是电视机、洗衣机还是电冰箱,只要是两相插头的设备均可以进行适配,所以说它是一种通用的适配器。

专属适配

除了上面所讲的“对象适配器”,我们还可以用“类适配器”实现接口的匹配,这是实现适配器模式的另一种方式。顾名思义,既然是类适配器,那么一定是属于某个类的“专属适配器”,也就是在编码阶段已经将被匹配的设备与目标接口进行对接了。我们继续之前的例子

package adapter;

public class TVAdapter extends TV implements  TriplePin{
    @Override
    public void electrify(int l, int n, int e) {
        super.electrify(l,n);
    }
}

类适配器模式实现起来更简单,如代码清单10-7所示,电视机专属适配器类中并未包含被适配对象(如电视机)的引用,而是在开始定义类的时候就直接继承自电视机了,此外还一并实现了三相插孔接口。接着在第4行的三插通电方法中,我们利用“super”关键字调用父类(电视机类TV)定义的两插通电方法,以实现适配。下面我们来使用这个类适配器

package adapter;

public class Client {
    public static void main(String[] args) {
//        TriplePin device = new TV();//接口不兼容
//        DualPin dualPinDevice = new TV();
//        TriplePin triplePinDevice = new Adapter(dualPinDevice);//适配器驳斥两端
//        triplePinDevice.electrify(1,0,-1);//此处调用的是三插通电标准
        //输出结果
        //火线通电:1,零线通电:0,
        //电视开机
        TriplePin tvAdapter  = new TVAdapter();//电视机专属三插适配器插入三相插孔
        tvAdapter.electrify(1,0,-1);//此处调用的是三插通电标准
    }
}

然而,这个类适配器是继承自电视机的子类,在类定义的时候就已经与电视机完成了接驳,也就是说,类适配器与电视机的继承关系让它固化为一种专属适配器,这就造成了继承耦合,倘若我们需要适配其他两插设备,它就显得无能为力了。例如要适配两相插头的洗衣机,我们就不得不再写一个“洗衣机专属适配器”,这显然是一种代码冗余,说明适配器兼容性差。

当然,事物没有绝对的好与坏,对象适配器与类适配器各有各的适用场景。假如我们只需要匹配电视机这一种设备,并且未来也没有任何其他的设备扩展需求,那么类适配器使用起来可能更加简便,所以具体用什么、怎么用还要视具体情况而定,切不要有过分偏执、非黑即白的思想。

化解难以调和的矛盾

众所周知,反复修改代码的代价是巨大的,因为所有依赖关系都要受到牵连,这不但会引入更多没有必要的重构与测试工作,而且其波及范围难以估量,可能会带来不可预知的风险,结果得不偿失。适配器模式让兼容性问题在不必修改任何代码的情况下得以解决,其中适配器类是核心,我们首先来看对象适配器模式的类结构

Target(目标接口):客户端要使用的目标接口标准,对应本章例程中的三相插孔接口TriplePin。

Adapter(适配器):实现了目标接口,负责适配(转换)被适配者的接口specificRequest()为目标接口request(),对应本章例程中的电视机专属适配器类TVAdapter。

Adaptee(被适配者):被适配者的接口标准,目前不能兼容目标接口的问题接口,可以有多种实现类,对应本章例程中的两相插孔接口DualPin。

Client(客户端):目标接口的使用者。

下面是类适配器模式的类结构

Target(目标接口):客户端要使用的目标接口标准,对应本章例程中的三相插孔接口TriplePin。

Adapter(适配器):继承自被适配者类且实现了目标接口,负责适配(转换)被适配者的接口specificRequest()为目标接口request()。

Adaptee(被适配者):被适配者的类实现,目前不能兼容目标接口的问题类,对应本章例程中的电视机类TV。

Client(客户端):目标接口的使用者。

对象适配器模式与类适配器模式基本相同,二者的区别在于前者的Adaptee(被适配者)以接口形式出现并被Adapter(适配器)引用,而后者则以父类的角色出现并被Adapter(适配器)继承,所以前者更加灵活,后者则更为简便。其实不管何种模式,从本质上看适配器至少都应该具备模块两侧的接口特性,如此才能承上启下,促成双方的顺利对接与互动

成功利用适配器模式对系统进行扩展后,我们就不必再为解决兼容性问题去暴力修改类接口了,转而通过适配器,以更为优雅、巧妙的方式将两侧“对立”的接口“整合”在一起,顺利化解双方难以调和的矛盾,最终使它们顺利接通

GO版本代码

通用适配模式

package adapter

import (
    "fmt"
    "strconv"
)

type DualPin interface {
    Electrify(l, n int)
}
type TriplePin interface {
    Electrify(l, n, e int)
}

type TV struct {
}

func (t *TV) Electrify(l, n int) {
    fmt.Printf("火线接通" + strconv.Itoa(l) + ",零线通电:" + strconv.Itoa(n))
    fmt.Println("电视开机")
}

type Adapter struct {
    DualPin
}

func NewAdapter(d DualPin) TriplePin {
    return &Adapter{
        DualPin: d,
    }
}
func (a *Adapter) Electrify(l, n, e int) {
    a.DualPin.Electrify(l, n)
}

专属适配模式

type TVAdapter struct {
    TV
}

func NewTVAdapter() TriplePin {
    return &TVAdapter{}
}
func (tv *TVAdapter) Electrify(l, n, e int) {
    tv.TV.Electrify(l, n)
}

main.go

package main

import "desginPatterns/adapter"

func main() {
    //通用适配
    dualPinDevice := &adapter.TV{}
    triplePinDevice := adapter.NewAdapter(dualPinDevice)
    triplePinDevice.Electrify(1, 0, -1)

    //专属适配
    tvAdapter := adapter.NewTVAdapter()
    tvAdapter.Electrify(1, 0, -1)

}