设计原则:面向接口编程、开闭原则、依赖倒置
做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);
}
}
这个例子中,支付工厂使用了注册模式。新加一种支付方式时,只需要调用register方法,不需要修改工厂本身。这就是对扩展开放,对修改关闭。
依赖倒置:让高层模块不再依赖底层细节
依赖倒置原则是面向接口编程的进阶版。它的核心是:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
是不是有点绕?我举个例子你就明白了。
假设我们有一个OrderService(高层模块),它需要把订单信息存到数据库。如果我们直接在OrderService里调用MysqlOrderRepo(低层模块),那就是高层依赖了低层。哪天要换成MongoDB,OrderService就得改。
依赖倒置的做法是:定义一个OrderRepository接口,让OrderService依赖这个接口,让MysqlOrderRepo和MongoOrderRepo也都依赖这个接口。所有依赖都指向了中间的抽象层。
这样设计的好处很明显:
- 替换成本低:换数据库时,接口不变,换实现就行。
- 测试方便:可以用内存型Repo做单元测试,不用真的连数据库。
- 并行开发:接口定好后,高层和低层可以同时开发。
核心区别:依赖注入(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平台时,完全不用动业务层代码,只需要新写一个实现类。
下面这张图展示了这三条原则在封装层中的协同关系:
图中可以看到,业务层依赖的是中间的抽象接口层,而不是下面的具体实现。具体实现可以任意扩展,只要实现了接口就行。这就是三个原则共同作用的结果。
嗯,掌握这三个原则,你的封装层就有了「可扩展」的骨架。下一块要聊的,就是怎么把这个骨架变成一个活的系统——适配器模式。其实很多所谓的「适配困难」,都是因为一开始没遵守这些原则。现在明白还来得及。