第3章 抽象层设计原则:依赖倒置、接口隔离、开闭原则
好,咱们进入正题。抽象层到底该怎么设计?不止一个同学问过我:「我照着网上的模板写了个抽象接口,怎么越写越别扭?」嗯,问题往往出在——你只抄了形式,没理解背后的三条原则。
依赖倒置、接口隔离、开闭原则,这三条是面向对象设计的核心,也是抽象层的骨架。我这些年重构过的密码SDK,凡是后期还能轻松扩展的,无一不是把这三点吃透了。你想想看,如果一开始方向就歪了,后面每加一个算法都要推翻重来,那还不如不抽象。
3.1 依赖倒置原则
依赖倒置说了啥?简单讲:高层模块不应该依赖低层模块,二者都应该依赖抽象。换句话说,你业务代码里不要new一个具体的AES对象,而是依赖一个抽象的Cipher接口。
举个例子。早期我写过一个密码工具类,里面直接new了AESProvider。后来客户要求加SM4,我改了一堆业务代码。那叫一个难受。后来改成这样:
// 抽象层
public interface CipherProvider {
byte[] encrypt(byte[] data, byte[] key);
byte[] decrypt(byte[] data, byte[] key);
}
// 高层业务模块
public class SecurityService {
private CipherProvider cipherProvider;
// 依赖注入,不依赖具体实现
public SecurityService(CipherProvider cipherProvider) {
this.cipherProvider = cipherProvider;
}
public byte[] protectData(byte[] data, byte[] key) {
return cipherProvider.encrypt(data, key);
}
}
注意看,SecurityService只认识CipherProvider这个抽象。你底层换成AesProvider还是Sm4Provider,它根本不关心。这就是依赖倒置——把依赖关系从「具体实现」拧到「抽象接口」上。
3.2 接口隔离原则
接口隔离原则说:客户端不应该被迫依赖它不需要的方法。说白了,别搞大而全的万能接口。
我记得有次接手一个遗留密码SDK,里面一个CryptoService接口包含了加密、解密、签名、验签、哈希、密钥派生……整整十几个方法。每个实现类都得实现一堆空方法,抛出UnsupportedOperationException。维护成本极高。
正确的做法是拆分成小接口:
public interface Encryptor {
byte[] encrypt(byte[] data, byte[] key);
}
public interface Decryptor {
byte[] decrypt(byte[] data, byte[] key);
}
public interface Signer {
byte[] sign(byte[] data, PrivateKey key);
boolean verify(byte[] data, byte[] signature, PublicKey key);
}
// 某些算法可能同时支持加密和签名
public interface AesCipher extends Encryptor, Decryptor {
// 不强制暴露签名方法
}
这样一来,你的业务代码只需要依赖它真正用到的接口。比如日志模块只加密,你给它一个Encryptor就够,它不需要知道还有签名这回事。
3.3 开闭原则
开闭原则:对扩展开放,对修改关闭。你新增一个密码算法,不需要改动已有的任何代码,只加新类就行。
拿我们前几章的密码SDK架构来说,扩展一个新算法只需要三步:
- 实现CipherProvider接口
- 在工厂或者注册表中注册新实现
- 客户端通过配置或枚举选择新算法
代码示例:
public class Sm4Provider implements CipherProvider {
@Override
public byte[] encrypt(byte[] data, byte[] key) {
// SM4加密实现
}
@Override
public byte[] decrypt(byte[] data, byte[] key) {
// SM4解密实现
}
}
// 注册到工厂,无需修改已有代码
public class CipherFactory {
private static final Map<String, CipherProvider> registry = new HashMap<>();
static {
registry.put("AES", new AesProvider());
registry.put("SM4", new Sm4Provider());
// 新增算法只需在这里加一行
}
public static CipherProvider getProvider(String algorithm) {
return registry.get(algorithm);
}
}
你看,新增SM4时,AesProvider和SecurityService一行代码都没动。这就是开闭原则的威力。
| 原则 | 核心思想 | 在密码SDK中的体现 |
|---|---|---|
| 依赖倒置 | 依赖抽象而非具体 | SecurityService依赖CipherProvider接口,而非AesProvider类 |
| 接口隔离 | 小接口,各自专精 | Encryptor、Decryptor、Signer各自独立,不强迫实现不需要的方法 |
| 开闭原则 | 扩展开放,修改关闭 | 新增Sm4Provider,不修改现有SecurityService和AesProvider |
3.4 三大原则在密码SDK中的协同
三条原则不是孤立的,它们共同作用才能让抽象层优雅又灵活。依赖倒置定义了「高层只认抽象」的依赖方向;接口隔离保证了抽象是轻量干净的;开闭原则确保了抽象可以无限扩展而不破坏已有代码。
下面这张图把三者的关系画在了一起。你可以看到:高层模块依赖抽象接口,抽象接口被拆成多个小接口,具体实现通过注册机制做到开闭。