设计原则:面向接口编程、开闭原则、依赖倒置

做SDK封装,最怕什么?

怕业务方说「换个云厂商,代码全得重写」。

怕自己改了一行,别的地方跟着崩。

怕封装层越改越臃肿,最后谁也看不懂。

这些坑,我都踩过。后来发现,根治的办法不是靠「小心点」,而是靠三个设计原则。说白了,就是给代码定几条规矩。规矩立好了,扩展性就有了,维护成本就低了。

我个人习惯,接手任何一个封装项目,先问自己三句话:

  • 接口是不是够抽象?
  • 新增功能能不能不修改旧代码?
  • 高层模块是不是只依赖接口,不依赖具体实现?

这三句话,对应的正是面向接口编程、开闭原则和依赖倒置。今天我们就逐个拆开来讲。

面向接口编程:先定契约,再写实现

很多新手写代码,上来就new一个具体类。比如封装短信SDK,直接用AliyunSmsSender。这样做快是快,但耦合也深。一旦要换成腾讯云的短信,得改多少地方?

我记得在做一个多语言SDK适配的项目时,同事就很喜欢直接调用具体实现类。后来客户要求换供应商,他改了一个通宵。我过去一看,代码里到处是new AliyunSmsSender(),每一个调用点都要改。

面向接口编程,核心就一句话:调用方只依赖接口,不依赖具体实现。

我的习惯做法:先定义接口,把「能做什么」列清楚,然后再去做实现类。接口就是契约,双方都按契约办事。

来看一个典型的反例和正例对比:

反例(依赖具体类) 正例(依赖接口)
代码中直接使用AliyunSmsSender 接口SmsSender定义send方法
更换供应商要改所有调用点 只需增加新的实现类,调用方不变
无法进行单元测试(依赖真实服务) 可以用Mock实现类做单元测试

你想想看,如果我们在一开始就定义一个SmsSender接口,业务层只引用这个接口。那么,阿里云、腾讯云、华为云,都只是这个接口的一个实现罢了。切换的时候,换一个实现类,改一行配置就完事了。

开闭原则:对扩展开放,对修改关闭

开闭原则,说白了就是:加新功能,不碰旧代码。

是不是听起来很理想?其实做起来有套路。核心在于:把「变化」和「不变」分开。

我曾经接手过一个支付SDK的封装。原来的代码把所有支付方式的逻辑都写在了一个类里,用if-else区分微信、支付宝、银联。每次新增一种支付方式,就要改这个类。改到后面,那个类有2000多行,谁都不敢动。

怎么救?抽象出一个Payment接口,每种支付方式一个实现类。新增支付方式时,不需要碰任何旧代码,只需要增加一个实现类,然后在配置里注册就可以了。

下面是一个简单的代码示意:

// 接口
public interface Payment {
    boolean pay(Order order);
}

// 微信支付实现
public class WechatPayment implements Payment {
    public boolean pay(Order order) {
        // 微信支付逻辑
        return true;
    }
}

// 支付宝支付实现
public class AlipayPayment implements Payment {
    public boolean pay(Order order) {
        // 支付宝支付逻辑
        return true;
    }
}

// 支付工厂
public class PaymentFactory {
    private Map<String, Payment> paymentMap = new HashMap<>();
    
    public void register(String type, Payment payment) {
        paymentMap.put(type, payment);
    }
    
    public Payment getPayment(String type) {
        return paymentMap.get(type);
    }
}
注意:开闭原则不是禁止修改代码。bug修复、性能优化,该改还得改。它禁止的是「因为新增功能而修改现有稳定代码」。

这个例子中,支付工厂使用了注册模式。新加一种支付方式时,只需要调用register方法,不需要修改工厂本身。这就是对扩展开放,对修改关闭。

依赖倒置:让高层模块不再依赖底层细节

依赖倒置原则是面向接口编程的进阶版。它的核心是:

  • 高层模块不应该依赖低层模块,两者都应该依赖抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象。

是不是有点绕?我举个例子你就明白了。

假设我们有一个OrderService(高层模块),它需要把订单信息存到数据库。如果我们直接在OrderService里调用MysqlOrderRepo(低层模块),那就是高层依赖了低层。哪天要换成MongoDB,OrderService就得改。

依赖倒置的做法是:定义一个OrderRepository接口,让OrderService依赖这个接口,让MysqlOrderRepo和MongoOrderRepo也都依赖这个接口。所有依赖都指向了中间的抽象层。

这样设计的好处很明显:

  1. 替换成本低:换数据库时,接口不变,换实现就行。
  2. 测试方便:可以用内存型Repo做单元测试,不用真的连数据库。
  3. 并行开发:接口定好后,高层和低层可以同时开发。

核心区别:依赖注入(DI)是实现依赖倒置的常见方式。把依赖的创建和注入交给容器或工厂,而不是在高层模块内部new出来。

我习惯用构造器注入:

// 高层模块只依赖抽象
public class OrderService {
    private final OrderRepository orderRepository;
    private final Payment payment;
    
    // 构造器注入,依赖由外部提供
    public OrderService(OrderRepository orderRepository, Payment payment) {
        this.orderRepository = orderRepository;
        this.payment = payment;
    }
    
    public void placeOrder(Order order) {
        orderRepository.save(order);
        payment.pay(order);
    }
}

这样设计的OrderService,根本不知道底层用的是MySQL还是MongoDB,也不知道支付用的是微信还是支付宝。它只知道自己依赖两个接口。这就是依赖倒置的威力。

三个原则怎么一起用?

面向接口编程、开闭原则、依赖倒置,这三个原则不是孤立的。它们像是一个组合拳:

  • 先面向接口编程,定义好契约(接口)。
  • 用开闭原则指导设计,让新增功能不碰旧代码。
  • 用依赖倒置确保高层模块依赖抽象,不依赖具体实现。

我在一个实际项目中,用这三板斧重构了一个对接多个AI平台的SDK。原来每个AI平台都有一套独立的调用逻辑,代码耦合严重。后来抽象出AIModel接口,、文心一言、通义千问各是一个实现类。业务层只依赖AIModel接口。新增一个AI平台时,完全不用动业务层代码,只需要新写一个实现类。

下面这张图展示了这三条原则在封装层中的协同关系:

三条设计原则协作架构图 业务调用层(高层模块) 抽象接口层(契约) 阿里云实现 腾讯云实现 华为云实现 三条原则的协同效果 面向接口编程 → 定义清晰的契约 开闭原则 → 新增实现不修改旧代码

图中可以看到,业务层依赖的是中间的抽象接口层,而不是下面的具体实现。具体实现可以任意扩展,只要实现了接口就行。这就是三个原则共同作用的结果。

嗯,掌握这三个原则,你的封装层就有了「可扩展」的骨架。下一块要聊的,就是怎么把这个骨架变成一个活的系统——适配器模式。其实很多所谓的「适配困难」,都是因为一开始没遵守这些原则。现在明白还来得及。