DTO、VO、Entity、Domain 与防腐层:数据模型为什么不能混用
几个对象字段几乎相同,为什么不能只保留一个通用 Model? 如果答案只落在“大家遵守约定”,边界就会随人员、交付压力和代码规模一起失效。模型不是字段容器,而是某个边界内的语义、身份、生命周期和不变量。复用同一个类会把数据库、协议、领域和展示的变化原因合并,使任何一侧的字段调整穿透所有层。
结构质量首先是一张可计算的图
代码结构可以表示为有向图:类、包或模块是节点,import、字段类型、方法签名、继承、注解、调用和反射配置形成边。目录树只显示节点被放在哪里,依赖图才显示谁真正知道谁。所谓“内层”“外层”也不是视觉位置,而是边被允许指向的方向。
接口 DTO 承担线格式与输入 presence,应用 Command/Result 表达用例,Domain Entity/Value Object 维护身份与不变量,Persistence Entity 适配表结构,外部系统模型经 ACL 转换。映射点完成校验、规范化、默认值、权限裁剪和版本翻译。
从一次变更追踪耦合怎样扩散
对每个模型记录 owner、identity、mutability、lifecycle、validation authority 和 serialization boundary。只有语义与生命周期同时一致才共享类型;重复几行字段通常比共享错误语义便宜。转换器必须有双向/单向决策和损失说明。
把 JPA Entity 直接返回会泄露持久化字段和延迟加载;用 DTO 承担领域行为会绕过不变量;自动映射按同名复制会把单位、null 和枚举差异静默吞掉;通用 BaseEntity 会让技术字段污染所有模型。
这类失败常被误诊为“开发不规范”,实际根因通常是规则不可执行、职责没有 owner 或历史债务没有迁移路径。正确修复不是再写一页规范,而是把允许边、禁止边、转换点、事务 owner 和例外期限放进代码与流水线。
模块边界要同时控制可见性和依赖方向
模块 API 应以用例能力或稳定领域概念组织,不暴露数据库 Entity、框架 Context、第三方 SDK 和内部异常。跨模块同步调用需要明确超时与失败;跨模块事件需要身份与版本;同进程直接调用也不能假装没有边界。模块化单体的价值正是用低分布式成本练习真实依赖纪律。
模型转换是边界的收费站
DTO、Command、Domain、Persistence Entity 与外部模型即使字段相同,也可能有不同的身份、可变性、生命周期和缺失语义。显式转换让这些差异有落点:单位换算、时间规范化、枚举未知值、权限裁剪、默认值、旧版本兼容和领域构造失败。
转换器要声明是否无损。Domain → DTO 通常是裁剪或格式化,DTO → Domain 需要验证并可能失败;Entity → Domain 要恢复不变量,Domain → Entity 可能包含版本和审计字段。不要设计一个双向万能 mapper 掩盖不对称。
Service、Repository 与 Mapper 按决策所有权分工
配置、错误、日志和测试必须有统一默认语义
配置需要固定源优先级、类型绑定、未知键策略、范围/组合校验和启动失败规则。字符串散落在业务代码会让同一配置被不同模块以不同默认值解释。密钥不进入普通配置回显,动态配置还要有版本、原子快照、回调隔离和失败保留旧值策略。
静态检查要按可见事实选择工具
高置信安全/正确性问题可立即阻断;大量历史风格问题适合 baseline 后增量阻断;实验规则先度量误报。规则失败消息应包含 ruleId、违规边、为何危险、允许替代和例外入口,否则开发者只会添加 suppress。
可运行模型一:让结构退化变成数字
javac --release 17 -Xlint:all -Werror ModelLeakageDemo.java
java ModelLeakageDemo预期输出:
fields=12 sharedAcrossLayers=9 leakedPersistenceFields=3 couplingScore=0.750 boundaryClean=false模型把“感觉不整洁”转换为依赖违规、字段泄漏、事务 owner、未知配置、质量预算或例外状态。反向实验要修改一个输入使门禁从通过变失败,并解释失败为何对应真实风险。只输出一个综合分数会掩盖根因,计数必须能回到具体边、字段、用例或规则。
可运行模型二:证明失败不会被平均值隐藏
javac --release 17 -Xlint:all -Werror MappingInvariantDemo.java
java MappingInvariantDemo预期输出:
inputs=6 accepted=4 rejected=2 normalized=3 domainInvariantsHeld=true第二个模型专门保留少数失败:一个循环、两处 SQL 泄漏、一个缺失上下文、一项未锁定依赖或一个无 owner 例外。质量门禁不能因为总体通过率很高就放过系统性缺口;某些不变量是 all-or-nothing。
依赖治理要看解析结果,不只看声明
漏洞优先级结合可达性、运行环境、数据暴露和修复可用性;unused 依赖仍扩大供应链与镜像;重复库可能引起类冲突。升级不是“版本越新越好”,而是小批次更新、兼容测试、行为对比和可回滚制品。
技术债必须有预算、趋势和终止条件
债务项记录 fingerprint、ruleId、scope、owner、risk、createdReason、compensation、targetCondition 与 expiry。历史 baseline 允许系统渐进治理,但新增违规不得进入 baseline;修复旧债时当前总数下降,质量门禁不能因仍未归零而阻断所有进展。
预算至少区分高风险正确性债、结构债、依赖债、测试债和可读性债。严重度乘数量的简单总分会让大量格式问题稀释一个安全缺陷。门禁采用分层阈值:高风险零容忍,中风险不得新增,低风险看趋势和变更触达范围。
模板、评审与例外形成闭环
模板提供最小可运行骨架:模块边界、依赖约束、类型化配置、错误入口、日志字段、测试层和构建门禁。模板不预装所有组件,不复制业务层空壳,也不生成无法删除的抽象。生成后就是普通代码,持续接受同一门禁。
评审根据风险分级。低风险由自动检查和一名 owner 验证;涉及数据迁移、公开契约、并发、一致性、权限或不可逆变更时,必须给出反向实验、指标、灰度和回滚。评审者验证证据能否支撑不变量,而不是重新编写提交者方案。
例外不是口头批准或永久 suppress。它具有规则、范围、owner、原因、风险、补偿控制、到期和删除条件;到期后流水线自动失败或要求重新评估。全局例外风险最大,优先缩小到文件、模块、依赖坐标或具体违规指纹。
安全与可观测不能被“代码质量”稀释
质量信号本身要可观测:规则通过率、baseline 总量与新增量、循环数、模块 API 面积、依赖更新年龄、未知配置、错误上下文缺失、flaky test 和例外到期。趋势按模块和 owner 展示,不用一个全项目“质量分”驱动表面优化。
门禁失败保存可行动证据而非完整源码或秘密。依赖报告与 SBOM 需要访问控制;日志样本脱敏;评审记录避免复制客户数据。质量平台拥有读取代码的高权限,其插件、规则包和生成模板也必须锁版本、审计来源并最小授权。
架构决策记录应该留下什么
记录模块节点、允许边、公开 API、模型 owner、转换损失、事务 owner、查询边界、配置优先级、错误目录、日志字段、测试层、规则能力、baseline、解析依赖图、例外与回滚。每条约束都链接到自动检查或反向实验。
模型不是字段容器,而是某个边界内的语义、身份、生命周期和不变量。复用同一个类会把数据库、协议、领域和展示的变化原因合并,使任何一侧的字段调整穿透所有层。 对这一主题,最终必须守住:跨边界转换显式且可验证,任何外部或持久化字段都不能绕过领域不变量。如果结构只能从架构图看出、无法从构建产物和测试证明,它还不是工程事实。
模块依赖图可导出,允许边、违规边与强连通分量可定位。public/internal API 与反射 opens 范围经过审查,不以全 public 省事。DTO、Domain、Persistence 和外部模型有明确 owner 与转换失败语义。
应用服务、领域、Repository、Mapper 和 Query Service 的所有权唯一。配置未知键与组合错误阻止启动,错误身份和日志脱敏稳定。测试层对应真实边界,不共享可变 Fixture 或生产特权分支。
静态规则匹配其可观察事实,历史 baseline 不吸收新增违规。declared/resolved/locked 依赖图可复现,升级和漏洞有验证证据。技术债与例外有 owner、补偿、期限和可执行关闭条件。
高风险变更没有反向实验、观测和回滚证据就不能发布。两个 Java 17 模型严格编译运行,输出与正文一致。
实现依据
以下官方资料用于核对语言可见性、架构检查和依赖解析能力;具体工具只实现部分规则,不替代工程边界本身:
Java Language Specification: Accessibility。ArchUnit package and class dependency checks
