如何在业务开发中使用适配器模式?

3,927 阅读7分钟

我正在参加「掘金·启航计划」

适配器模式(Adapter Pattern):将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

说人话:这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。比如现实生活中的例子, 就像我们提到的万能充、数据线、MAC笔记本的转换头、出国旅游买个插座等等,他们都是为了适配各种不同的口 ,做的兼容。

适配器模式定义

image.png

Target目标角色:该角色定义把其他类转换为何种接口, 也就是我们的期望接口, 例子中的IUserInfo接口就是目标角色。

Adaptee源角色:你想把谁转换成目标角色, 这个“谁”就是源角色, 它是已经存在的、 运行良好的类或对象, 经过适配器角色的包装, 它会成为一个崭新、 靓丽的角色。

Adapter适配器角色:适配器模式的核心角色, 其他两个角色都是已经存在的角色, 而适配器角色是需要新建立的, 它的职责非常简单: 把源角色转换为目标角色, 怎么转换? 通过继承或是类关联的方式。

通用代码实现

/**
 * 目标角色
 */
public interface Target {
    void t1();
    void t2();
    void t3();
}
/**
 * 目标角色实现类
 */
public class ConcreteTarget implements Target{

    @Override
    public void t1() {
        System.out.println("目标角色 t1 方法");
    }

    @Override
    public void t2() {
        System.out.println("目标角色 t2 方法");
    }

    @Override
    public void t3() {
        System.out.println("目标角色 t3 方法");
    }
}
/**
 * 源角色:要把源角色转换成目标角色
 */
public class Adaptee {

    public void a1(){
        System.out.println("源角色 a1 方法");
    }

    public void a2(){
        System.out.println("源角色 a2 方法");
    }

    public void a3(){
        System.out.println("源角色 a3 方法");
    }
}

基于继承的类适配器

/**
 * 适配器角色
 */
public class Adapter extends Adaptee implements Target{

    @Override
    public void t1() {
        super.a1();
    }

    @Override
    public void t2() {
        super.a2();
    }

    @Override
    public void t3() {
        super.a3();
    }
}

基于组合的对象适配器

public class AdapterCompose implements Target{

    private Adaptee adaptee;

    public AdapterCompose(Adaptee adaptee){
        this.adaptee = adaptee;
    }
    @Override
    public void t1() {
        adaptee.a1();
    }

    @Override
    public void t2() {
        adaptee.a2();
    }

    @Override
    public void t3() {
        adaptee.a3();
    }
}

测试

public class AdapterClient {

    public static void main(String[] args) {
        // 原有的业务逻辑
        Target target = new ConcreteTarget();
        target.t1();

        // 基于继承 增加适配器业务逻辑
        Target target1 = new Adapter();
        target1.t1();

        // 基于组合 增加适配器业务逻辑
        Target target2 = new AdapterCompose(new Adaptee());
        target2.t1();
    }
}

打印结果:

目标角色 t1 方法
源角色 a1 方法
源角色 a1 方法

适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。在实际开发中,选择的依据如下:

1、如果 Adaptee 接口并不多,那两种实现方式都可以。

2、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。

3、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

适用场景

1、修改已使用的接口,某个已经投产中的接口需要修改,这时候使用适配器最好。

2、统一多个类的接口设计,比如对于敏感词过滤,需要调用好几个第三方接口,每个接口方法名,方法参数又不一样,这时候使用适配器模式,将所有第三方的接口适配为统一的接口定义。

3、兼容老版本接口。

4、适配不同格式的数据。

案例场景分析

现在假设⼀个系统需要接收各种各样的MQ消息或者接⼝,如果⼀个个的去开发,就会耗费很⼤的成本,同时对于后期的拓展也有⼀定的难度。此时就会希望有⼀个系统可以配置⼀下就把外部的MQ接⼊进⾏,这些MQ就像上⾯提到的可能是⼀些注册开户消息、商品下单消息等等。

⽽适配器的思想⽅式也恰恰可以运⽤到这⾥,并且我想强调⼀下,适配器不只是可以适配接⼝往往还可以适配⼀些属性信息。

92e2d638162facefd7d360311762a81.png

一坨坨代码实现

这⾥模拟了三个不同类型的MQ消息,⽽在消息体中都有⼀些必要的字段,⽐如;⽤户ID、时间、业务ID,但是每个MQ的字段属性并不⼀样。就像⽤户ID在不同的MQ⾥也有不同的字段:uId、userId等。

注册开户MQ

public class CreateAccount {

    private String number;      // 开户编号
    private String address;     // 开户地
    private Date accountDate;   // 开户时间
    private String desc;        // 开户描述

 	// ... get/set
}

内部订单MQ

public class OrderMq {

    private String uid;           // 用户ID
    private String sku;           // 商品
    private String orderId;       // 订单ID
    private Date createOrderTime; // 下单时间

 	// ... get/set
}

第三⽅订单MQ

public class POPOrderDelivered {

    private String uId;     // 用户ID
    private String orderId; // 订单号
    private Date orderTime; // 下单时间
    private Date sku;       // 商品
    private Date skuName;   // 商品名称
    private BigDecimal decimal; // 金额

    // ... get/set
}

Mq接收消息实现

public class CreateAccountMqService {
    
    public void onMessage(String message) {
        CreateAccount mq = JSON.parseObject(message, CreateAccount.class);
        mq.getNumber();
        mq.getAccountDate();
        // ... 处理自己的业务
    }
}

三组MQ的消息都是⼀样模拟使⽤,就不⼀⼀展示了。

适配器模式重构

统⼀的MQ消息体

public class RebateInfo {

    private String userId;  // 用户ID
    private String bizId;   // 业务ID
    private Date bizTime;   // 业务时间
    private String desc;    // 业务描述

	// ... get/set
}

MQ消息中会有多种多样的类型属性,虽然他们都有同样的值提供给使⽤⽅,但是如果都这样接⼊那么当MQ消息特别多时候就会很麻烦。

所以在这个案例中我们定义了通⽤的MQ消息体,后续把所有接⼊进来的消息进⾏统⼀的处理。

MQ消息体适配类

public class MQAdapter {

    public static RebateInfo filter(String strJson, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return filter(JSON.parseObject(strJson, Map.class), link);
    }

    public static RebateInfo filter(Map obj, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        RebateInfo rebateInfo = new RebateInfo();
        for (String key : link.keySet()) {
            Object val = obj.get(link.get(key));
            RebateInfo.class.getMethod("set" + key.substring(0, 1).toUpperCase() + key.substring(1), String.class).invoke(rebateInfo, val.toString());
        }
        return rebateInfo;
    }
}

主要⽤于把不同类型MQ种的各种属性,映射成我们需要的属性并返回。就像⼀个属性中有 ⽤户ID;uId ,映射到我们需要的 userId ,做统⼀处理。

⽽在这个处理过程中需要把映射管理传递给 Map<String, String> link ,也就是准确的描述了,当前MQ中某个属性名称,映射为我们的某个属性名称。

最终因为我们接收到的 mq 消息基本都是 json 格式,可以转换为MAP结构。最后使⽤反射调⽤的⽅式给我们的类型赋值。

在实际业务开发中,除了反射的使用外,还可以加入代理类把映射的配置交给它。这样就可以不需要每一个mq都手动创建类了。

测试类

public class ApiTest {

    @Test
    public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ParseException {
        SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date parse = s.parse("2023-02-27 20:20:16");

        CreateAccount createAccount = new CreateAccount();
        createAccount.setNumber("1000");
        createAccount.setAddress("北京");
        createAccount.setAccountDate(parse);
        createAccount.setDesc("在校开户");

        HashMap<String, String> link01 = new HashMap<String, String>();
        link01.put("userId", "number");
        link01.put("bizId", "number");
        link01.put("bizTime", "accountDate");
        link01.put("desc", "desc");
        RebateInfo rebateInfo01 = MQAdapter.filter(createAccount.toString(), link01);
        System.out.println("mq.createAccount(适配前)" + createAccount.toString());
        System.out.println("mq.createAccount(适配后)" + JSON.toJSONString(rebateInfo01));
    }
}
mq.createAccount(适配前){"accountDate":1677500416000,"address":"北京","desc":"在校开户","number":"1000"}
mq.createAccount(适配后){"bizId":"1000","bizTime":1591077840669,"desc":"在校开户","userId":"1000"}

模拟传⼊不同的MQ消息,并设置字段的映射关系。等真的业务场景开发中,就可以配这种映射配置关系交给配置⽂件或者数据库后台配置,减少编码。

总结

1、将目标类和适配者类解耦,通过使用适配器让不兼容的接口变成了兼容,让客户从实现的接口解耦。

2、增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。

3、灵活性和扩展性都非常好在不修改原有代码的基础上增加新的适配器类,符合“开闭原则”。