第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,它根本不关心。这就是依赖倒置——把依赖关系从「具体实现」拧到「抽象接口」上。

个人建议:依赖倒置的最佳实践是配合依赖注入容器(比如Spring的IoC)。但你手动构造函数注入也行,只要保证业务代码里不出现具体实现类的import。

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就够,它不需要知道还有签名这回事。

我曾经踩过的坑:接口拆得太细也会出问题。比如拆成Encryptor、Decryptor、EncryptDecryptor三个接口,反而导致组合爆炸。通常拆到每个接口2-4个方法最合适,别超过5个。

3.3 开闭原则

开闭原则:对扩展开放,对修改关闭。你新增一个密码算法,不需要改动已有的任何代码,只加新类就行。

拿我们前几章的密码SDK架构来说,扩展一个新算法只需要三步:

  1. 实现CipherProvider接口
  2. 在工厂或者注册表中注册新实现
  3. 客户端通过配置或枚举选择新算法

代码示例:

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中的协同

三条原则不是孤立的,它们共同作用才能让抽象层优雅又灵活。依赖倒置定义了「高层只认抽象」的依赖方向;接口隔离保证了抽象是轻量干净的;开闭原则确保了抽象可以无限扩展而不破坏已有代码。

下面这张图把三者的关系画在了一起。你可以看到:高层模块依赖抽象接口,抽象接口被拆成多个小接口,具体实现通过注册机制做到开闭。

三大设计原则在密码SDK中的协同 三大设计原则在密码SDK抽象层中的协同 高层业务模块 (SecurityService) 依赖 «接口» Encryptor «接口» Decryptor «接口» Signer 接口隔离原则:拆分为小接口 AesProvider Sm4Provider RsaSigner 开闭原则:新增实现不修改已有代码 依赖倒置:高层依赖抽象 设计原则协同总结 依赖倒置保证高层不受底层变更影响;接口隔离保证抽象边界清晰;开闭保证扩展零侵入。 三者共同作用,打造可维护、可演进的密码SDK抽象层。
一句话总结:依赖倒置定方向,接口隔离定边界,开闭原则定扩展。设计抽象层时,拿这三把尺子量一下你的接口——如果哪条没满足,重构的信号就亮了。