将账单清洗逻辑封装为具备状态与行为的不可变实体类(如cleanedbill),按职责边界拆分为validator、sanitizer、normalizer等内聚组件,通过接口隔离策略并提供语义化方法,实现高内聚、低耦合、易测试的业务模型。

直接把账单清洗逻辑从服务层或工具类里抽出来,封装进一个专门的实体类,是解决臃肿问题最自然的路径。关键不是“加个类”,而是让这个类真正代表一个业务概念——比如 BillCleaner 或 CleanedBill,它知道自己该管什么、不该暴露什么。
识别清洗逻辑中的真实职责边界
物理账单清洗往往混着字段校验、格式标准化、空值填充、金额单位转换、敏感信息脱敏等动作。这些不是同一类责任:
- 字段校验(如发票号非空、日期合法)属于数据合规性,适合封装为
BillValidator - 手机号掩码、身份证脱敏属于隐私处理,应独立为
BillSanitizer - 金额统一转为分、时间转为标准时区,属于单位与格式归一化,可抽象为
BillNormalizer
如果全塞在 BillService.clean() 里,就等于让一个类承担三类变化原因——规则变、合规要求变、财务系统对接变。拆开后,每个类只响应自己那条线的变更。
用实体类承载状态+行为,而非仅做数据容器
避免贫血模型:不要只建一个 CleanedBillDTO,再配一堆静态工具方法。应该建一个有生命力的实体:
- 构造时接收原始账单数据(
RawBill),内部完成一次完整清洗流程 - 提供
isValid()、getNormalizedAmount()、getRedactedContact()等语义清晰的方法,而不是暴露setAmountCents(int)或getPhoneMasked() - 所有字段声明为
private final,初始化后不可变;清洗失败时抛出明确异常(如InvalidBillException),不留下半成品对象
封装细节,只暴露稳定契约
外部调用者不需要知道清洗用了正则还是规则引擎,也不关心脱敏是用星号还是哈希。实体类对外接口应体现业务意图:
- 坏设计:
cleanWithRegex(String pattern)、applyMaskRule(int level) - 好设计:
toStandardForm()(返回新实例)、withPrivacyProtection()(链式调用)、asFinalizedBill()(含校验+归一+脱敏全流程) - 内部可自由替换实现:今天用
PhoneNumberFormatter,明天换成IntlPhoneParser,只要输出一致,外部完全无感
配合接口隔离,支持灵活组合与测试
清洗流程常需按场景切换策略(如对公账单 vs 个人账单)。这时用接口定义契约,实体类实现具体行为:
- 定义
BillCleaningPolicy接口,含shouldValidate()、needsRedaction()等方法 -
CleanedBill构造时注入策略,将判断逻辑外移,自身专注执行 - 单元测试时可传入模拟策略,验证不同分支下的清洗结果,无需启动整个账单上下文
重构后,账单清洗不再是散落在各处的 if-else 和字符串拼接,而是一个有边界、可验证、易替换的业务实体。它不依赖 Spring 容器,不耦合数据库,甚至能脱离 JVM 在单元测试中跑通全部逻辑。











