去年接了个二手电商的私活,订单状态有12个:待支付、已支付、待发货、已发货、待收货、已完成、已取消、退款中、退款成功、退款失败、售后中、售后完成。一开始用if-else,嵌套了7层,改一个状态提心吊胆。后来听人说策略模式好,就把每个状态写成一个策略类,结果更糟——12个策略类里都有switch判断当前状态该转到哪个状态,新增“部分退款”时改了8个文件,漏了一个导致线上订单卡在退款中。那晚我盯着屏幕,感觉被设计模式坑了。
后来我翻了GoF的《设计模式》,状态模式那章开头写着“允许一个对象在其内部状态改变时改变其行为”。我突然意识到,策略模式和状态模式虽然接口长得像,但职责完全不同。策略模式是外部选择:客户端知道有哪些策略,主动挑一个。比如排序,你选冒泡还是快排。状态模式是内部驱动:客户端不知道状态,状态自己决定下一步。比如Promise,pending变成fulfilled,不是外部选的,是异步操作完成了。用比喻:策略模式像你去餐厅点菜,你选红烧肉还是回锅肉;状态模式像你吃饭的过程,饿了→点菜→吃→饱了,状态自动流转。关键区别:策略模式的接口方法通常不返回下一个策略,状态模式的接口方法要负责转移状态。
我重构的步骤是这样的:第一步,画状态转移图,明确每个状态能转到哪些状态,比如待支付只能转已支付或已取消。第二步,定义状态接口,包含handle方法,参数是订单上下文。第三步,每个状态实现类,在handle里处理业务,并调用context.setState(nextState)。第四步,上下文类持有当前状态,提供setState方法,对外只暴露handle。第五步,客户端只调context.handle(),不需要知道任何状态。重构后代码从1200行降到350行,新增状态只要加一个类,修改转移逻辑只改相关状态类。但也不是没坑:状态类之间如果互相调用,会形成网状依赖,最好用状态机框架,比如Spring StateMachine,不过小项目手动实现够用了。
那到底怎么选?我总结了5个判断点:第一,谁决定变化?外部参数选策略,内部事件选状态。第二,状态之间是否互斥?策略可以同时存在,状态同一时刻只有一个。第三,是否需要状态转移图?需要就选状态模式。第四,新增一个状态/策略的成本?策略只需加类,状态要考虑转移关系。第五,状态数量是否超过15个?超过就考虑状态机框架。我的独立观点:很多团队用策略模式硬套状态流转,是因为策略模式简单,但长期维护成本高。状态模式不是银弹,但用对了能减少70%的条件判断。另外,别迷信设计模式,有时候一个状态枚举加switch,比强行套模式更清晰。