分层、模块化与依赖方向:代码边界怎样阻止耦合扩散
目录看起来分层,为什么一次改动仍会穿透整个系统? 如果答案只落在“大家遵守约定”,边界就会随人员、交付压力和代码规模一起失效。分层的本质是允许哪些代码知道哪些代码。边界只有被编译可见性、模块 API 和自动依赖规则共同约束时才存在;包名和架构图本身不会阻止反向依赖。
结构质量首先是一张可计算的图
代码结构可以表示为有向图:类、包或模块是节点,import、字段类型、方法签名、继承、注解、调用和反射配置形成边。目录树只显示节点被放在哪里,依赖图才显示谁真正知道谁。所谓“内层”“外层”也不是视觉位置,而是边被允许指向的方向。
编译器从 import、继承、字段、方法签名、注解和调用建立类型依赖;构建系统把模块依赖映射为 classpath/module path;运行时再通过反射、SPI 或序列化形成隐式边。依赖方向决定变化向哪里传播,环则让原本独立的发布、测试和理解单元重新粘成整体。
从一次变更追踪耦合怎样扩散
先按业务变化原因切模块,再定义每个模块的 public API、internal 包、允许依赖和事件/端口。用有向图检查入边、出边、强连通分量和不稳定依赖;迁移时冻结既有债务但禁止新增,并逐步收紧。
Controller 直接引用 Mapper、domain 依赖 Spring/JPA、公共模块收纳任意 DTO、双向模块调用和反射绕过可见性都会让内层依赖外层。只画分层图而不扫描字节码,违规会在重构中持续累积。
这类失败常被误诊为“开发不规范”,实际根因通常是规则不可执行、职责没有 owner 或历史债务没有迁移路径。正确修复不是再写一页规范,而是把允许边、禁止边、转换点、事务 owner 和例外期限放进代码与流水线。
模块边界要同时控制可见性和依赖方向
模型转换是边界的收费站
“减少重复”不能只数同名字段。共享一个类会让所有调用方共享它的注解、序列化、equals/hashCode、验证和发布节奏。真正重复是同一知识被多处维护;如果两个字段代表不同边界的知识,重复声明反而隔离变化。映射代码可以生成,但映射规则和失败语义必须审查。
Service、Repository 与 Mapper 按决策所有权分工
配置、错误、日志和测试必须有统一默认语义
配置需要固定源优先级、类型绑定、未知键策略、范围/组合校验和启动失败规则。字符串散落在业务代码会让同一配置被不同模块以不同默认值解释。密钥不进入普通配置回显,动态配置还要有版本、原子快照、回调隔离和失败保留旧值策略。
静态检查要按可见事实选择工具
高置信安全/正确性问题可立即阻断;大量历史风格问题适合 baseline 后增量阻断;实验规则先度量误报。规则失败消息应包含 ruleId、违规边、为何危险、允许替代和例外入口,否则开发者只会添加 suppress。
可运行模型一:让结构退化变成数字
运行 DependencyDirectionDemo.java:
javac --release 17 -Xlint:all -Werror DependencyDirectionDemo.java
java DependencyDirectionDemo预期输出:
modules=6 edges=8 allowed=7 violations=1 inwardOnly=false模型把“感觉不整洁”转换为依赖违规、字段泄漏、事务 owner、未知配置、质量预算或例外状态。反向实验要修改一个输入使门禁从通过变失败,并解释失败为何对应真实风险。只输出一个综合分数会掩盖根因,计数必须能回到具体边、字段、用例或规则。
可运行模型二:证明失败不会被平均值隐藏
javac --release 17 -Xlint:all -Werror CycleDetectionDemo.java
java CycleDetectionDemo预期输出:
nodes=6 edges=7 stronglyConnectedComponents=4 cyclicComponents=1 cycle=[billing, inventory, promotion] deployable=false第二个模型专门保留少数失败:一个循环、两处 SQL 泄漏、一个缺失上下文、一项未锁定依赖或一个无 owner 例外。质量门禁不能因为总体通过率很高就放过系统性缺口;某些不变量是 all-or-nothing。
依赖治理要看解析结果,不只看声明
漏洞优先级结合可达性、运行环境、数据暴露和修复可用性;unused 依赖仍扩大供应链与镜像;重复库可能引起类冲突。升级不是“版本越新越好”,而是小批次更新、兼容测试、行为对比和可回滚制品。
技术债必须有预算、趋势和终止条件
预算至少区分高风险正确性债、结构债、依赖债、测试债和可读性债。严重度乘数量的简单总分会让大量格式问题稀释一个安全缺陷。门禁采用分层阈值:高风险零容忍,中风险不得新增,低风险看趋势和变更触达范围。
模板、评审与例外形成闭环
模板提供最小可运行骨架:模块边界、依赖约束、类型化配置、错误入口、日志字段、测试层和构建门禁。模板不预装所有组件,不复制业务层空壳,也不生成无法删除的抽象。生成后就是普通代码,持续接受同一门禁。
评审根据风险分级。低风险由自动检查和一名 owner 验证;涉及数据迁移、公开契约、并发、一致性、权限或不可逆变更时,必须给出反向实验、指标、灰度和回滚。评审者验证证据能否支撑不变量,而不是重新编写提交者方案。
例外不是口头批准或永久 suppress。它具有规则、范围、owner、原因、风险、补偿控制、到期和删除条件;到期后流水线自动失败或要求重新评估。全局例外风险最大,优先缩小到文件、模块、依赖坐标或具体违规指纹。
安全与可观测不能被“代码质量”稀释
质量信号本身要可观测:规则通过率、baseline 总量与新增量、循环数、模块 API 面积、依赖更新年龄、未知配置、错误上下文缺失、flaky test 和例外到期。趋势按模块和 owner 展示,不用一个全项目“质量分”驱动表面优化。
门禁失败保存可行动证据而非完整源码或秘密。依赖报告与 SBOM 需要访问控制;日志样本脱敏;评审记录避免复制客户数据。质量平台拥有读取代码的高权限,其插件、规则包和生成模板也必须锁版本、审计来源并最小授权。
架构决策记录应该留下什么
记录模块节点、允许边、公开 API、模型 owner、转换损失、事务 owner、查询边界、配置优先级、错误目录、日志字段、测试层、规则能力、baseline、解析依赖图、例外与回滚。每条约束都链接到自动检查或反向实验。
分层的本质是允许哪些代码知道哪些代码。边界只有被编译可见性、模块 API 和自动依赖规则共同约束时才存在;包名和架构图本身不会阻止反向依赖。 对这一主题,最终必须守住:业务内核不依赖交付与基础设施,模块依赖图没有未登记反向边和环。如果结构只能从架构图看出、无法从构建产物和测试证明,它还不是工程事实。
应用服务、领域、Repository、Mapper 和 Query Service 的所有权唯一。配置未知键与组合错误阻止启动,错误身份和日志脱敏稳定。测试层对应真实边界,不共享可变 Fixture 或生产特权分支。
静态规则匹配其可观察事实,历史 baseline 不吸收新增违规。declared/resolved/locked 依赖图可复现,升级和漏洞有验证证据。技术债与例外有 owner、补偿、期限和可执行关闭条件。
高风险变更没有反向实验、观测和回滚证据就不能发布。两个 Java 17 模型严格编译运行,输出与正文一致。
实现依据
以下官方资料用于核对语言可见性、架构检查和依赖解析能力;具体工具只实现部分规则,不替代工程边界本身:
Java Language Specification: Packages and Modules。ArchUnit User Guide
