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

状态机简介

这里所说的状态机,全名为确定性有穷状态自动机,也常被简称为有穷自动机,简写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层,这取决于你如何设计的系统的架构。这里做事件触发时,需要传入一个上下文信息,来告知状态机当前的初态和事件,也可以传入一些自定义的内容,以便业务逻辑执行时使用。

@Service
public 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
@Slf4j
public 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),更多干货实践,欢迎交流分享~

猜你喜欢

转载自juejin.im/post/7121678614893953038