ZhuanzhuanYanxuanオーダーがステートマシンに遭遇したとき

ステートマシンの概要

ここで言及されているステートマシン、フルネームは決定論的有限状態オートマトンであり、しばしば有限オートマトン、または略してFSMとも呼ばれます。ソフトウェアの分野では、コンパイル、正規表現認識、ゲーム開発など、広く利用されています。ステートマシンは、状態コレクションとイベントコレクションのセットを維持します。これらは、特定のイベントを入力し、状態フローを作成し、対応するアクションを実行できます。

ステートマシン要素

  • 状態セット(状態)
  • イベントコレクション(イベント)
  • 検出器(ガード)
  • トランジション
  • 環境

ビジネスシステムの使用範囲

インターネットビジネスシステムでは、複雑な状態のドキュメントを含むすべてのビジネスシナリオでステートマシンを使用できます。

ビジネスシステムアプリケーション

ビジネスシステムでは、状態ジャンプ図の設定と状態ジャンプのビジネスロジックカプセル化により、特定のイベント(インターフェース要求)を入力し、状態ジャンプを行い、対応するビジネスロジックを実行することができます。

ステートマシンの利点

まず第一に、どのようなコードが良いコードであるか、最も直感的な感覚は、それを一目で理解できるということです、それはいわゆる明確な論理ですつまり、それはコード表現のアイデアです、これはほとんどの人が問題について考える方法と一致しています。人々は自然に複雑なものや単純なものにうんざりしているため、複雑な問題を直接解決することは困難です。複雑な問題は、多くの単純な問題の組み合わせと見なされることがよくあります。長い練習の中で、私たちは基本を学び、単純な問題の解決策を徐々に習得しましたが、複雑な問題の場合は、複雑な問題を単純な問題に分割する方法も習得する必要があります。これは分割の考え方です。コードの構造については、まだこのアイデアを使用して考えています。ビジネスロジックが複雑になった瞬間から、人々は問題を単純化する方法を考えてきました。ポイントのアイデアにより、ビジネスロジック、制御ロジック、およびデータを分離するための階層化アーキテクチャが登場しました。ビジネスロジックがさらに複雑な場合は、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