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

适配器模式,说白了就是“转接头”。你想想看,Type-C 转 HDMI、两脚插头转三脚插座——现实世界里到处都是适配器。软件里也一样:你有一个老系统,接口是 A;新模块只认接口 B。硬改?成本太高。写个适配器?嗯,这才是正经路子。

我个人习惯把适配器模式分成三类:类适配器、对象适配器、双向适配器。别被名字吓到,核心就一句话——让原本不兼容的接口能一起工作

核心定义:将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

1. 类适配器(继承)

类适配器用继承来实现。适配器继承目标接口,同时继承被适配者的实现。好处是代码少,坏处是 Java 单继承,不够灵活。

我记得有一次接手一个遗留的日志系统,它只输出到文件。新需求要求同时输出到网络。我不想动老代码,就写了个类适配器:

// 目标接口:新系统需要的日志接口
public interface Logger {
    void log(String message);
}

// 被适配者:老系统的文件日志
public class FileLogger {
    public void writeToFile(String msg) {
        System.out.println("写入文件: " + msg);
    }
}

// 类适配器:继承老类,实现新接口
public class LoggerAdapter extends FileLogger implements Logger {
    @Override
    public void log(String message) {
        // 直接调用父类方法
        writeToFile(message);
    }
}

用起来很简单:

Logger logger = new LoggerAdapter();
logger.log("用户登录成功");
我的经验:类适配器适合被适配者方法不多、且你不需要适配多个类的情况。如果被适配者有一堆 protected 方法,继承反而容易暴露内部细节。

2. 对象适配器(组合)

对象适配器用组合来实现。适配器持有被适配者的引用,通过委托调用。这是我最推荐的方式——组合优于继承,这话我说了十年都不腻。

同样是上面的日志例子,用对象适配器改写:

// 对象适配器:持有被适配者实例
public class LoggerAdapter2 implements Logger {
    private FileLogger fileLogger;

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

    @Override
    public void log(String message) {
        fileLogger.writeToFile(message);
    }
}

用起来:

FileLogger oldLogger = new FileLogger();
Logger logger = new LoggerAdapter2(oldLogger);
logger.log("用户退出系统");
避坑指南:我曾经在项目中用类适配器适配了三个不同的老接口,结果继承链乱成一锅粥。后来全部改成对象适配器,每个适配器只负责一个接口,清晰多了。记住:组合是解药,继承是毒药(大部分时候)

3. 双向适配器

双向适配器有点意思。它同时实现两个接口,既能当 A 用,也能当 B 用。说白了就是“两头通吃”。

场景:你有一个老系统的支付接口,一个新系统的支付接口。两边都想调用对方。写一个双向适配器:

// 老支付接口
public interface OldPayment {
    void pay(double amount);
}

// 新支付接口
public interface NewPayment {
    void processPayment(double amount);
}

// 双向适配器
public class PaymentAdapter implements OldPayment, NewPayment {
    private OldPayment oldPayment;
    private NewPayment newPayment;

    // 适配老系统到新系统
    public PaymentAdapter(OldPayment oldPayment) {
        this.oldPayment = oldPayment;
    }

    // 适配新系统到老系统
    public PaymentAdapter(NewPayment newPayment) {
        this.newPayment = newPayment;
    }

    @Override
    public void pay(double amount) {
        // 老接口调用 -> 转成新接口
        if (newPayment != null) {
            newPayment.processPayment(amount);
        } else {
            oldPayment.pay(amount);
        }
    }

    @Override
    public void processPayment(double amount) {
        // 新接口调用 -> 转成老接口
        if (oldPayment != null) {
            oldPayment.pay(amount);
        } else {
            newPayment.processPayment(amount);
        }
    }
}
使用场景:双向适配器在系统迁移时特别好用。老系统和新系统并行运行,两边互相调用,适配器做桥梁。等老系统下线,直接去掉适配器就行。

4. 遗留系统接口适配案例

这是适配器模式最经典的战场。我参与过一个银行核心系统改造项目,老系统暴露了一堆 EJB 接口,新系统用 RESTful API。不改老系统,新系统又等不及——适配器模式救场。

老接口:

// 老系统:EJB 风格的账户查询
public interface LegacyAccountService {
    AccountDTO getAccountByCustomerId(String customerId);
    List<TransactionDTO> getTransactions(String accountId, Date from, Date to);
}

新接口:

// 新系统:REST 风格的账户查询
public interface AccountService {
    AccountVO queryAccount(String customerId);
    List<TransactionVO> queryTransactions(String accountId, String fromDate, String toDate);
}

适配器:

public class AccountServiceAdapter implements AccountService {
    private LegacyAccountService legacyService;

    public AccountServiceAdapter(LegacyAccountService legacyService) {
        this.legacyService = legacyService;
    }

    @Override
    public AccountVO queryAccount(String customerId) {
        AccountDTO dto = legacyService.getAccountByCustomerId(customerId);
        // 转换 DTO 到 VO
        return convertToVO(dto);
    }

    @Override
    public List<TransactionVO> queryTransactions(String accountId, String fromDate, String toDate) {
        Date from = parseDate(fromDate);
        Date to = parseDate(toDate);
        List<TransactionDTO> dtos = legacyService.getTransactions(accountId, from, to);
        return dtos.stream().map(this::convertToVO).collect(Collectors.toList());
    }

    private AccountVO convertToVO(AccountDTO dto) {
        // 字段映射逻辑
        return new AccountVO(dto.getId(), dto.getName(), dto.getBalance());
    }

    private TransactionVO convertToVO(TransactionDTO dto) {
        return new TransactionVO(dto.getId(), dto.getAmount(), dto.getDate().toString());
    }

    private Date parseDate(String dateStr) {
        // 简单日期解析
        return new Date();
    }
}
关键点:适配器里不要写业务逻辑。只做接口转换和数据映射。业务逻辑应该留在被适配者或目标系统里。

5. 适配器模式 vs 其他模式

模式 目的 结构 典型场景
适配器 接口转换 适配器包装被适配者 遗留系统集成
代理 控制访问 代理持有真实对象 延迟加载、权限控制
装饰器 增强功能 递归包装 动态添加职责
外观 简化接口 门面封装子系统 复杂系统简化调用

你可能会问:适配器和代理看起来好像?区别在于意图。适配器是为了让接口匹配,代理是为了控制访问。适配器改接口,代理不改接口。

6. 适配器模式的核心结构

下面这张图帮你理清适配器模式的核心逻辑。我画了对象适配器的结构,因为这是最常用的形式:

对象适配器模式结构图 目标接口 Target request() 客户端 Client 适配器 Adapter request() adaptee.specificRequest() 被适配者 Adaptee specificRequest() 实现 持有 调用 工作流程 1. 客户端调用目标接口的 request() 2. 适配器将请求委托给被适配者的 specificRequest() 3. 客户端无需知道被适配者的存在 组合关系(实线箭头) vs 实现关系(虚线箭头)

7. 总结与避坑

适配器模式不是什么高深技术,但用好了能省很多事。我总结几条实战经验:

  • 优先用对象适配器:组合比继承灵活,尤其 Java 单继承限制下。
  • 适配器里别写业务逻辑:只做接口转换和数据映射。业务逻辑一进去,适配器就变成了“四不像”。
  • 注意性能损耗:每次调用都经过适配器,会有轻微性能开销。高并发场景下要评估。
  • 不要过度适配:如果接口差异太大,适配器会变得臃肿。这时候考虑重构被适配者或目标接口。
我曾经踩过的坑:在一个项目中,我用适配器包装了三个不同版本的支付接口。结果每个接口的异常处理方式不同,适配器里塞满了 if-else 和 try-catch。后来我统一了异常处理规范,在适配器外层加了一个异常转换层,代码才清爽起来。记住:适配器不是万能胶,接口差异太大时,先统一规范再适配

适配器模式,说白了就是“让旧代码活过来”的魔法。你不需要重写老系统,也不需要等新系统重构。写个适配器,两边都能跑。嗯,这就是我为什么一直推荐它的原因。


公众号:蓝海资料掘金营,微信deep3321