3. 接口分层设计原则:高内聚、低耦合与单一职责
接口分层设计,说白了就是在划分模块边界时,让每个模块各司其职,彼此又不互相拖累。这三条原则——高内聚低耦合、单一职责、接口隔离——听起来像老生常谈,但在DLP(数据泄露防护)这种涉及大量敏感数据类型和策略组合的场景里,它们就是架构的命门。
3.1 高内聚低耦合:模块的“收”与“放”
高内聚,是说一个模块内部的东西应该紧密相关。低耦合,是模块之间依赖要少、要清晰。我个人习惯用一个比喻:高内聚像一把瑞士军刀,每个工具都为了“切割”服务;低耦合像不同品牌的充电器,只靠一个标准接口交互。
在DLP接口分层里,我建议把数据采集、策略匹配、日志记录当成三个独立模块。注意,这不是简单分文件,而是从依赖关系上彻底解耦。
具体怎么做?
- 数据采集层:只负责从数据源(文件、网络、数据库)读取原始内容,输出统一的
RawContent对象。 - 策略匹配层:只消费
RawContent,执行正则、关键字、指纹等匹配逻辑,输出MatchResult。 - 日志记录层:只接受
MatchResult,做持久化和告警通知。
我见过很多新手架构师犯的一个错:在数据采集层里直接写日志。一旦后续要换日志中间件,采集层代码也得跟着改。你想想看,这本来不该是采集层操心的。
3.2 单一职责原则:一个接口,一个变化原因
单一职责原则(SRP)要求每个接口(或类、模块)只负责一个明确的业务维度。在DLP接口设计里,这条原则最容易执行,也最容易被人忽略。
为什么会这样?因为它太“简单”了。简单到很多人觉得“差不多就行”,结果越积越重。
| 违反SRP的接口 | 问题表现 |
|---|---|
| 一个接口同时处理策略加载和策略匹配 | 策略源从本地文件换成数据库时,匹配逻辑也要跟着改 |
| 一个接口同时处理数据加密和日志记录 | 加密失败时,如何保证日志不丢?两者生命周期不同 |
| 一个接口同时处理协议解析和内容检测 | 协议变了(HTTP→FTP),内容检测模块不得不重新编译 |
我在项目里制定过一条硬规矩:每个DLP接口只能回答一个问题。比如:
ContentParser.parse()只负责从字节流提取文本PolicyEvaluator.evaluate()只负责文本是否命中策略AuditLogger.log()只负责把结果写进存储
嗯,这里要注意一点: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个,那我就得重新审视模块边界了。