写点什么

当转转严选订单遇到状态机

  • 2022 年 7 月 18 日
  • 本文字数:3633 字

    阅读完需:约 12 分钟

当转转严选订单遇到状态机

状态机简介

这里所说的状态机,全名为确定性有穷状态自动机,也常被简称为有穷自动机,简写 FSM。在软件领域中,被广泛应用,如编译,正则表达式识别,游戏开发。状态机维护一组状态集合,和事件集合,能够对特定的事件输入,作出状态流转,并执行相应的动作。


状态机要素

  • 状态集合(states)

  • 事件集合(events)

  • 检测器(guards)

  • 转换器(transitions)

  • 上下文(context)

业务系统使用范围

在互联网业务系统中,所有涉及到包含复杂状态的单据的业务场景,都可以使用状态机。

业务系统应用

在业务系统中,通过对状态跳转图的配置,以及对状态跳转的业务逻辑封装,完成系统对某一个特定的事件输入(接口请求),做出状态的跳转和对应业务逻辑的执行。


状态机的优势

首先说什么样的代码是好代码,最直观的感受就是,一看就懂,就是所谓的逻辑清晰,换句话说,就是代码表达的思路,符合大多数人对于问题的思考方式。人天生就对复杂的东西感到厌恶,喜欢简单的东西,这就决定了,人很难直接解决复杂的问题。而复杂的问题,往往可以看成是很多简单问题的组合。在漫长的实践中,我们学习基础,慢慢的掌握了对简单问题的解决方案,然而对于复杂问题,我们还需要掌握如何把复杂问题拆分成一个一个简单问题,这也就是分的思想。对于代码的架构,我们依然是使用这个思想来思考的。从业务逻辑开始变得复杂那一刻起,人们就不断在思考如何将问题变得简单,用分的思想,出现了分层架构,将业务逻辑,控制逻辑与数据分离。当业务逻辑进一步复杂时,我们用 DDD 的思想,将复杂的业务逻辑,拆分到一个个领域对象的行为当中,再通过领域服务用易理解的方式将它们组织在一起。



在业务系统中,应用状态机,不仅仅意味着,使用了状态机的组件,还意味着代码架构的选型,可以称之为“状态机架构”。因为它能够很好的拆分了我们的代码逻辑,并通过特定的机制将不同的模块连接在一起,完成系统的功能。这也就是状态机在系统中起到的指导设计的作用。它能让我们的系统,在长久的迭代中,保持基本的逻辑拆分原则。



除了拆分业务逻辑外,它还把控制逻辑和业务逻辑进行了分离。我们上学时都学过这样一个等式,程序=数据结构+算法,后来一位名为 Robert Kowalski 的大师进一步论证了,算法=逻辑+控制,并提出如果逻辑和控制分离的话,将会让代码更好维护。在业务逻辑复杂的今天,我们本就应该专注于编写业务逻辑,控制逻辑就应该交尽量交给框架去解决。状态机的引入,也就意味着,它帮我们分担了这部分工作。



最后,直观上看,我们的代码基本消除了对状态判断的 if...else...代码,这些代码穿插在业务逻辑中,就好比你坐在桌前思考问题时,有一只苍蝇在你眼前晃来晃去。


// 随处可见的状态判断if (state == EOrderState.WAIT_PAY) {  ...} else if (state == EOrderState.PAYED) {  ...}// 下边执行业务逻辑...
复制代码


综述,状态机的引入,能够使代码的学习成本降低,也能够使维护成本降低。

状态机在转转严选交易的实践

理解业务

首先,需要绘制状态流转图。要整体理解业务,把单据按业务规则,抽象出一个一个状态,并且明确,什么动作可以促使它从一个状态变成另一个状态。比如,转转严选的订单,用户下单了之后,系统会生成一个订单,此时应该是“待支付”,那么当用户支付后,系统收到了支付消息,也就是收到了“支付”这个事件,就应该从待支付跳转到“已支付”。在这个过程中,我们看到的“状态”,椭圆一个一个画出来,我们提到的“事件”,用文字写在在两个可以流转的“状态”椭圆之间,然后把两个状态按照流转方向用箭头连接。把所有的状态和事件都画好后,状态机的状态跳转图就呈现出来了。


配置状态机

然后,根据把状态流转图,转换为我们的代码,为了便于理解,我们可以把代码设计的更加贴近自然语言。比如从“待支付”到“已支付”的代码可以是这样写的。


StateMachineConf conf = new StateMachineConf();conf.source(EOrderState.WAIT_PAY)    .onEvent(EOrderEvent.PAY)    .target(EOrderState.PAYED)    .transition(userPayTransition)    ...
复制代码


在这一个配置组合的编码之前,我们还需要把每一个状态,每一个事件都放到枚举里定义好。


/** * 状态定义 */public enum EOrderState implements IFSMState {    WAIT_PAY(1, "待支付"),    PAYED(2,"已支付"),    ...}
复制代码


/** * 事件定义 */public enum EOrderEvent implements IFSMEvent {    PAY(1, "用户支付"),    APPLY_FOR_REFUND(2,"申请退款"),    ...}
复制代码


把所有的状态和事件写好后,我们的配置代码是这样的:


StateMachineConf conf = new StateMachineConf();conf.source(s1).onEvent(e1).target(s2).transition(t1)    .and().source(s2).onEvent(e2).target(s3).transition(t2)    ...fsm.setName("转转严选订单状态机");fsm.config(conf);
复制代码

业务逻辑

状态机的 transition(转换器)是用来执行状态跳转时需要做的事情。所以,需要把我们的业务逻辑,写进对应的 transition 中,如果不需要执行动作,可以定义一个空的 transition


/** * 编写业务逻辑 */public class BuyerPayTransition implements IFSMTransition<OrderFSMContext> {    @Override    public boolean onGuard(OrderFSMContext context, IFSMState targetState) {        // 检测器逻辑,校验条件    }
@Override public void onTransition(OrderFSMContext context, IFSMState targetState) { // 转换器逻辑,业务逻辑在这里 }}
复制代码


下一步,就是状态机的触发了,也就是输入事件。这一部分逻辑,可以放到分层架构的 service 层,当然也可以放到 facade 层,这取决于你如何设计的系统的架构。这里做事件触发时,需要传入一个上下文信息,来告知状态机当前的初态和事件,也可以传入一些自定义的内容,以便业务逻辑执行时使用。


@Servicepublic class OrderService {    @Resource    private StateMachine fsm;
public void userPay(Order order) { OrderFSMContext context = new OrderFSMContext(); context.setSourceState(order.getState()); context.setEvent(EOrderEvent.PAY); context.setOrder(order); fsm.fire(context); }}
复制代码

异常情况

执行以上方法,状态机就会自动帮我们调用 BuyerPayTransition 中的逻辑。那么如果出现了异常情况会发生什么呢,比如当前订单已经退过款了,但是系统重复收到了一个退款事件,当然不能重复执行一次退款。首先为了防止并发问题,我们修改订单状态时,要使用类似于乐观锁的机制。


update order set state=3 where id=xxx and state=2;
复制代码


然后,状态机在选择逻辑时,发现初始状态为“已退款”,事件为“申请退款”,没有可以执行的逻辑分支,这个时候我们可以选择让状态机抛出异常,或者我们定义一个回调,来打印一些友好的信息,或做一些记录。


StateMachineConf conf = new StateMachineConf();conf.source(s1).onEvent(e1).target(s2).transition(t1)    .and().source(s2).onEvent(e2).target(s3).transition(t2)    ...fsm.setName("转转严选订单状态机");fsm.config(conf);fsm.setTransBlock(orderTransBlock);  // 这里配置无法跳转时的回调
复制代码


@Component@Slf4jpublic class OrderFSMTransBlock implements IFSMTransBlock<OrderFSMContext> {
@Override public boolean onTransBlock(OrderFSMContext context) { log.info("状态机无法跳转..."); }}
复制代码

可扩展性考虑

如果有一天,转转在售卖严选手机订单的同时,用户只需支付 1 元钱即可加购一个手机壳,并且在手机退款时,手机壳必须要同时帮用户退款,如何做呢。按照上边的设计思路,应该这样写:


public class ApplyForRefundTransition implements IFSMTransition<OrderFSMContext> {    @Override    public void onTransition(OrderFSMContext context, IFSMState targetState) {        // 处理手机退款逻辑        ...        // 处理手机壳退款逻辑        ...    }}
复制代码


虽然这样写没什么问题,但是这把两个业务流程耦合在一起了,如果明天需要再加个数据线,后天再加个贴膜...代码就会慢慢腐化,逻辑臃肿,架构坍塌。为了解决这个问题,我们可以设计一个注解,来监听严选手机订单的状态机动作。


@Transition(source = EOrderState.PAYED, event = EOrderEvent.APPLY_FOR_REFUND, fsm = "转转严选订单状态机")public void phoneShellRefund(OrderFSMContext context) {    // 处理手机壳退款逻辑}
复制代码

写在最后

状态机不是什么高级的技术,重点在于让你用另一种思路去理解,去设计系统,以达到我们想要的目的。生活亦是如此,换一种眼观去看待事物,去理解世界,我们能生活的更幸福。(全文完)


> 转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。> 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

发布于: 刚刚阅读数: 5
用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
当转转严选订单遇到状态机_架构_转转技术团队_InfoQ写作社区