学习 Java 对象建模时,布尔字段看起来是一种很直接的表达方式:是否已支付,是否已取消,是否已发货,每个问题都能回答是或否。然而,当几个字段共同描述同一个业务过程时,单独看都很清楚的答案,组合起来却可能互相矛盾。今天想整理的,就是怎样发现这些组合问题,并让对象能够表达哪些状态真实存在。
先设想一个简化的订单对象,只有是否已支付和是否已取消两个字段。两个字段一共可以形成四种组合,但业务是否允许支付后取消,需要另行定义。若继续加入是否已发货,组合数量还会增加。代码中能保存某个组合,并不代表业务上认可这个组合。状态表达与业务规则之间的这段距离,就是后来大量条件判断和修补逻辑产生的地方。
当然,也不能看到多个布尔字段就认为一定需要合并。有些属性确实彼此独立,例如是否需要纸质包装与是否允许短信提醒,未必属于同一条状态流转。判断的关键,是这些字段是否在共同回答“这个对象现在处于哪个阶段”,还是在描述不同维度的选择。如果把独立维度强行塞进一个状态字段,也会出现大量重复组合,反而让模型难以维护。
对于确实互斥的阶段,可以先写出具体名称,例如待付款、待处理、已发货和已取消,再讨论每个名称的含义。这样做的价值,是把原来分散在多个真假值中的业务事实集中起来。读到待付款时,应该清楚哪些操作可以进行,哪些条件仍然没有满足;读到已取消时,也要知道它是否代表整个流程已经结束。名字只有和明确的业务约定放在一起,才有实际作用。
接着需要讨论允许的变化。某个阶段可以转到哪里,由什么操作触发,是否需要满足额外条件,这些最好在修改字段之前就说明清楚。例如取消操作应检查当前阶段,发货操作应确认必要的前置条件。若任意调用方都能随意改写各个状态字段,对象就很难维持自己的约束。把变化放在含义清楚的业务操作里,可以让检查和修改更容易保持一致。
这里还要区分当前状态与历史事实。订单已经退款,并不代表它从未支付过;当前没有处于发货阶段,也不能简单推断它从未发货。若一个布尔字段的名字既像当前阶段,又像历史记录,阅读时很容易产生歧义。当前阶段可以服务于接下来允许做什么,历史事实则可能需要独立的时间记录或事件信息。两者回答不同问题,不应该为了减少字段而丢掉业务需要的信息。
状态变化也可能跨越多个维度。支付、履约和售后流程有时需要各自描述,不能全部压缩成一条线。例如商品已经发出,同时存在退款申请,在某些业务里是合理情况。遇到这种情况,可以分别建模各自的状态,再定义维度之间的约束。模型是否合适,要看它能否准确表达业务中的真实组合,而不是看一个对象最终只剩下几个字段。
已有数据的迁移也值得谨慎。把多个布尔字段改成明确的阶段之前,需要检查历史记录中究竟存在什么组合。某些组合可能来自旧规则,某些可能是异常数据,也可能只是当前理解遗漏了业务场景。不能在没有依据时,把所有无法识别的组合统一映射为正常状态。应先保留原始信息,再确定转换和人工复查规则,避免结构变得整齐以后,反而掩盖了需要处理的问题。
另外,对象内部的检查并不能自动解决并发修改。两个请求可能同时读到同一个旧阶段,然后分别执行不同操作。如果数据会被多个执行者共同修改,还需要在真正保存变化的位置维持约束,并根据实际结果处理竞争失败。业务方法让变化的含义清楚,存储层的协调机制则帮助这个变化在并发情况下按约定发生,它们的职责需要相互衔接。
学习时可以选一个简单流程,把字段可能形成的组合全部列出来,再标注哪些组合允许存在,哪些组合需要额外解释。随后画出允许的变化,检查每个操作是否都有明确的起点和结果。这个练习不要求一开始就设计复杂框架,却能让很多隐藏的假设暴露出来。对我来说,对象建模的重要目标,是让正常状态容易表达,让不成立的组合难以被随手创建;当字段真正对应业务含义,后续判断也会更容易读懂。