3. 接口分层设计原则:高内聚、低耦合与单一职责

接口分层设计,说白了就是在划分模块边界时,让每个模块各司其职,彼此又不互相拖累。这三条原则——高内聚低耦合、单一职责、接口隔离——听起来像老生常谈,但在DLP(数据泄露防护)这种涉及大量敏感数据类型和策略组合的场景里,它们就是架构的命门。

3.1 高内聚低耦合:模块的“收”与“放”

高内聚,是说一个模块内部的东西应该紧密相关。低耦合,是模块之间依赖要少、要清晰。我个人习惯用一个比喻:高内聚像一把瑞士军刀,每个工具都为了“切割”服务;低耦合像不同品牌的充电器,只靠一个标准接口交互。

在DLP接口分层里,我建议把数据采集、策略匹配、日志记录当成三个独立模块。注意,这不是简单分文件,而是从依赖关系上彻底解耦。

避坑指南:我曾经接手过一个遗留系统,数据采集模块里直接调了策略匹配的内部函数。结果每次改策略逻辑,采集模块也要重新测试。明明两个东西变化的频率和原因完全不同,硬绑在一起,改一处牵全身,维护成本翻倍。

具体怎么做?

  • 数据采集层:只负责从数据源(文件、网络、数据库)读取原始内容,输出统一的RawContent对象。
  • 策略匹配层:只消费RawContent,执行正则、关键字、指纹等匹配逻辑,输出MatchResult。
  • 日志记录层:只接受MatchResult,做持久化和告警通知。

我见过很多新手架构师犯的一个错:在数据采集层里直接写日志。一旦后续要换日志中间件,采集层代码也得跟着改。你想想看,这本来不该是采集层操心的。

3.2 单一职责原则:一个接口,一个变化原因

单一职责原则(SRP)要求每个接口(或类、模块)只负责一个明确的业务维度。在DLP接口设计里,这条原则最容易执行,也最容易被人忽略。

为什么会这样?因为它太“简单”了。简单到很多人觉得“差不多就行”,结果越积越重。

违反SRP的接口问题表现
一个接口同时处理策略加载和策略匹配策略源从本地文件换成数据库时,匹配逻辑也要跟着改
一个接口同时处理数据加密和日志记录加密失败时,如何保证日志不丢?两者生命周期不同
一个接口同时处理协议解析和内容检测协议变了(HTTP→FTP),内容检测模块不得不重新编译

我在项目里制定过一条硬规矩:每个DLP接口只能回答一个问题。比如:

  • ContentParser.parse() 只负责从字节流提取文本
  • PolicyEvaluator.evaluate() 只负责文本是否命中策略
  • AuditLogger.log() 只负责把结果写进存储
核心准则:如果你要给一个接口写超过两段“不同职责开头的注释”,它就很有可能违反了SRP。

嗯,这里要注意一点:SRP不意味着一个类只能有一个方法。而是这个类提供的所有方法,必须围绕同一个职责。比如PolicyEvaluator可以有evaluate_keyword、evaluate_regex、evaluate_fingerprint,但它们都服务于一个目标:判断内容是否违规。

3.3 接口隔离原则:客户端不依赖它不用的东西

接口隔离原则(ISP)强调的是接口的“精粒度”。一个大型的“万能接口”,会让实现方被迫实现一堆空方法,调用方也看不清楚自己到底依赖了什么。

我记得在DLP项目初期,团队设计了一个DLPService接口,里面列举了二十多个方法。所有客户端都依赖这个庞然大物,每次改一个小逻辑,所有调用方都要重新编译、重新测试。真是自找麻烦。

// 反例:胖接口
public interface DLPService {
    byte[] scanContent(byte[] data);
    void addPolicy(Policy p);
    PolicyResult evaluate(Policy p, String content);
    void archiveLog(String id);
    Report generateReport(Date start, Date end);
    // ... 还有十几个方法
}

// 正例:拆分后的接口
public interface ContentScanner {
    ScanResult scan(byte[] data);
}

public interface PolicyManager {
    void addPolicy(Policy p);
    void removePolicy(String id);
}

public interface LogArchiver {
    void archive(String logId);
    Report generateReport(Date start, Date end);
}

这么拆分之后,ContentScanner的调用方完全不关心策略是怎么管理的。LogArchiver的调用方也不依赖数据扫描逻辑。每个客户端只看自己需要的部分,改一处代码不会共振到不相关的地方。

注意:接口隔离不是让你把接口无限拆碎。粒度太细也会导致接口爆炸,反而增加调用方的组合成本。适度就好,我通常以“调用方是否真的会单独使用”作为判断标准。

三个原则放在一起看,它们其实是不同视角的同一个诉求:降低变更的传播范围,提高模块的可替换性。你想想看,DLP策略变更是常态,数据源格式也经常翻新,如果接口不分层、不隔离,每次变更是很容易撞到别人的职责上。

最后分享一个我在项目中常用的检查方法:每次设计完接口分层,我会问自己“如果明天要换一种数据源格式,我最多改几个文件?”如果答案超过2个,那我就得重新审视模块边界了。

接口分层设计三原则应用 高内聚低耦合 单一职责原则(SRP) 接口隔离原则(ISP) 数据采集层内聚,策略层解耦 变更影响范围收窄到单模块 每个接口只响应一个变化原因 策略与日志分离,加密与匹配解耦 胖接口拆分为最小依赖集 客户端只用自己关心的部分 降低模块间耦合度 提高接口可替换性 最终目标:独立的变更域 + 清晰的依赖边界