1. 接口适配器模式:让不兼容的接口“握手”

做系统架构这些年,我遇到最多的一个场景就是:两套系统明明功能对得上,接口却死活接不上。就像一个是USB-A,一个是Type-C,中间缺根转接线。适配器模式,说白了就是这根“转接线”。

今天咱们就把它拆开看看,从原理到实战,把五种适配器形态一次讲透。

1.1 适配器模式原理

适配器模式的核心意图很简单:把一个类的接口转换成客户端期望的另一个接口。它解决的是“接口不匹配”的问题,而不是“功能缺失”的问题。

我习惯用一个公式来记:

适配器 = 目标接口 + 被适配者 + 适配逻辑

三个角色缺一不可:

  • 目标接口(Target):客户端期望的接口规范
  • 被适配者(Adaptee):已经存在的、但接口不匹配的类
  • 适配器(Adapter):中间人,负责转换调用

举个例子。你写了一个日志系统,统一用 log(message) 方法。但第三方库只提供 writeToFile(filename, content)。怎么办?写个适配器,把 log 调用转成 writeToFile 调用。就这么简单。

1.2 类适配器 vs 对象适配器

这是适配器模式里最经典的二选一。我当年刚入行时也纠结过,后来踩了坑才明白它们的适用场景。

对比维度 类适配器 对象适配器
实现方式 继承被适配者 + 实现目标接口 持有被适配者引用 + 实现目标接口
耦合度 高(继承是强耦合) 低(组合是弱耦合)
灵活性 低(只能适配一个被适配者) 高(可适配多个被适配者)
代码量 少 稍多
适用场景 被适配者接口固定,不需要额外行为 需要适配多个被适配者,或需要动态切换

类适配器示例(Java):

// 目标接口
public interface LogTarget {
    void log(String message);
}

// 被适配者
public class FileLogger {
    public void writeToFile(String filename, String content) {
        // 写入文件逻辑
    }
}

// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends FileLogger implements LogTarget {
    @Override
    public void log(String message) {
        writeToFile("app.log", message);
    }
}

对象适配器示例(Java):

// 对象适配器:持有被适配者引用
public class ObjectAdapter implements LogTarget {
    private FileLogger fileLogger;

    public ObjectAdapter(FileLogger fileLogger) {
        this.fileLogger = fileLogger;
    }

    @Override
    public void log(String message) {
        fileLogger.writeToFile("app.log", message);
    }
}
我的建议:除非你明确知道被适配者不会变,否则优先选对象适配器。组合优于继承,这话在适配器模式里尤其适用。我在一个支付网关项目里用过类适配器,结果换了第三方库后,整个适配器都得重写——血的教训。

1.3 缺省适配器模式

你想想看,有时候一个接口定义了十几个方法,但你只需要实现其中一两个。每次都要写一堆空实现,烦不烦?

缺省适配器就是干这个的。它提供一个抽象类,把接口所有方法都实现成空方法或默认行为。你只需要继承这个抽象类,覆盖你关心的那几个方法就行。

// 一个有很多方法的接口
public interface EventListener {
    void onClick();
    void onDoubleClick();
    void onRightClick();
    void onHover();
    void onDrag();
    void onDrop();
}

// 缺省适配器:提供空实现
public abstract class DefaultEventListener implements EventListener {
    @Override public void onClick() {}
    @Override public void onDoubleClick() {}
    @Override public void onRightClick() {}
    @Override public void onHover() {}
    @Override public void onDrag() {}
    @Override public void onDrop() {}
}

// 使用时,只覆盖需要的方法
public class ClickListener extends DefaultEventListener {
    @Override
    public void onClick() {
        System.out.println("点击事件处理");
    }
}

我曾经在一个GUI框架里见过这种设计。框架定义了20多个事件回调,但大部分场景只需要处理点击和拖拽。缺省适配器让代码清爽了很多。

1.4 双向适配器

这个稍微进阶一点。双向适配器,就是适配器同时实现了两个接口,可以在两个方向上进行转换。

场景是这样的:系统A用接口A,系统B用接口B。有时候A要调用B,有时候B要调用A。写两个适配器?太麻烦。一个双向适配器搞定。

// 接口A
public interface InterfaceA {
    void methodA();
}

// 接口B
public interface InterfaceB {
    void methodB();
}

// 双向适配器
public class TwoWayAdapter implements InterfaceA, InterfaceB {
    private InterfaceA targetA;
    private InterfaceB targetB;

    public TwoWayAdapter(InterfaceA a, InterfaceB b) {
        this.targetA = a;
        this.targetB = b;
    }

    @Override
    public void methodA() {
        targetB.methodB(); // A调用转成B调用
    }

    @Override
    public void methodB() {
        targetA.methodA(); // B调用转成A调用
    }
}
注意:双向适配器容易引入循环调用。我在一个消息中间件项目里就踩过这个坑——A适配到B,B又适配到A,结果形成了死循环。解决方案是加一个调用标记,或者明确调用方向。

1.5 适配器在接口兼容中的应用

讲完理论,咱们看看实际场景。适配器模式在接口兼容性处理中,几乎是标配。

场景一:新旧系统对接

老系统用SOAP,新系统用REST。写个适配器,把SOAP请求转成REST请求,再把REST响应转回SOAP响应。我在一个银行核心系统迁移项目中就这么干过,适配器层跑了两年,直到老系统完全下线。

场景二:第三方SDK集成

不同厂商的SDK接口千奇百怪。统一封装一层适配器,对外暴露标准接口。换厂商时,只需要换适配器实现,业务代码不用动。

场景三:多数据源统一

MySQL、Redis、MongoDB,查询方式各不相同。适配器模式可以把它们统一成 get(key)、set(key, value) 这样的通用接口。

下面这张图,是我做接口适配时常用的结构设计:

适配器模式核心结构 客户端 目标接口 (Target) 适配器 (Adapter) 被适配者 (Adaptee) 依赖 实现 调用 关键说明: • 客户端只依赖目标接口,不直接接触被适配者 • 适配器负责将目标接口调用转换为被适配者的具体调用 • 被适配者可以是遗留系统、第三方库或任何不兼容的接口

小结

适配器模式不是什么高深的技术,但它解决的是系统集成中最常见的问题。我个人习惯把它当作“接口翻译官”——两边语言不通,中间站个人翻译一下。

记住几个要点:

  • 优先用对象适配器,组合比继承灵活
  • 缺省适配器能省掉大量空实现代码
  • 双向适配器小心循环调用
  • 适配器层是系统边界,做好隔离,换实现时业务代码不用动