从打车业务发展看技术的扩展性


从打车业务的发展,结合订单的设计演变看技术扩展性的重要性。对于业务来说,一般绕不开多场景、多状态,因此在技术设计之初要充分评估和考虑,保持扩展性又不过度设计,为未来能更快赋能业务打下坚实的基础。
Cola的状态机和扩展节点是基于业务抽象总结的,可以很好的解决这类场景,在实际开发中可以引入或者借鉴这种思想。

打车案例

假设新业务的发展需要经过三个阶段,接下来围绕几个阶段来介绍打车业务的发展和技术的演变过程。
新业务发展三个阶段

孵化期

孵化阶段需要快速迭代、快速试错来验证市场。

事业部:最近有个想法,出租车打车很不方便,得走到路边还经常打不到车,准备孵化个打车业务看看有没有市场,感觉这块蛋糕很大,你帮我设计下。
产品经理:好的,您有更详细的一些想法吗?
事业部:我的需求是用户可以在App上输入起点和重点,然后下单;司机可以在App上接单,接送后结单收钱。很简单,就这样,你先帮我细化设计下,设计完我们再碰一稿。
产品经理:好吧……(一口老血喷出来)

吐血

产品经理还是很专业的,按照事业部给的“抽象业务”进行了一番设计:
打车业务设计

需求评审之后,开发开始了表结构的设计,然后进行了分工协作,就开始编码了。为了更聚焦,我们以订单业务的代码为例来进行说明。

public class OrderServiceImpl {

    /**
     * 下单
     */
    public void placeOrder() {
        // TODO(xxx) 参数完整性校验
        // TODO(xxx) 设置状态为下单状态
        // TODO(xxx) 发送MQ
    }

    /**
     * 派单
     */
    public void sendOrder() {
        // TODO(xxx) 参数完整性校验
        // TODO(xxx) 派单给合适的司机
        // TODO(xxx) 更新订单状态为履约中
        // TODO(xxx) 发送MQ
    }

    /**
     * 取消订单
     */
    public void cancelOrder() {
        // TODO(xxx) 参数完整性校验,是否可取消
        // TODO(xxx) 取消订单逻辑
        // TODO(xxx) 更新订单状态为取消
        // TODO(xxx) 发送MQ
    }

    /**
     * 完成订单
     */
    public void finishOrder() {
        // TODO(xxx) 参数完整性校验
        // TODO(xxx) 完成订单逻辑
        // TODO(xxx) 更新订单状态为完成
        // TODO(xxx) 发送MQ
    }
}

成长期

经过2个月的开发,项目顺利上线。公司集中资源进行了大范围的推广,获得了很好的市场反应。试运行3个月后,公司针对该业务进行了复盘总结,觉得这个业务方向是没问题的,可以加大投入,在业务的深度(细分)和广度上(范围)进行扩展。于是有了以下对话:

事业部:打车业务经过3个月的试运行,得到了市场的验证。我们接下来决定对业务进行细分,类型有专车、快车,在业务上会有一些区分。
产品经理:没问题,我设计一下,保证您满意。

研发根据设计针对订单业务做了一番调整,每个业务操作的业务逻辑处理里通过if…else…进行了区分,如下:

public class OrderServiceImpl {

    /**
     * 下单
     */
    public void placeOrder() {
        // TODO(xxx) 参数完整性校验
        
        if(orderType.equals("专车")) {
            // TODO(xxx) 下单业务专车逻辑处理
        } else {
            // TODO(xxx) 下单业务快车逻辑处理
        }
        
        // TODO(xxx) 设置状态为下单状态
        // TODO(xxx) 发送MQ
    }

    /**
     * 派单
     */
    public void sendOrder() {
        // TODO(xxx) 参数完整性校验
        if(orderType.equals("专车")) {
            // TODO(xxx) 派单业务专车逻辑处理
        } else {
            // TODO(xxx) 派单业务快车逻辑处理
        }
        // TODO(xxx) 更新订单状态为履约中
        // TODO(xxx) 发送MQ
    }

    /**
     * 取消订单
     */
    public void cancelOrder() {
        // TODO(xxx) 参数完整性校验,是否可取消
        if(orderType.equals("专车")) {
            // TODO(xxx) 取消业务专车逻辑处理
        } else {
            // TODO(xxx) 取消业务快车逻辑处理
        }
        // TODO(xxx) 更新订单状态为取消
        // TODO(xxx) 发送MQ
    }

    /**
     * 完成订单
     */
    public void finishOrder() {
        // TODO(xxx) 参数完整性校验
        if(orderType.equals("专车")) {
            // TODO(xxx) 完成业务专车逻辑处理
        } else {
            // TODO(xxx) 完成业务快车逻辑处理
        }
        // TODO(xxx) 更新订单状态为完成
        // TODO(xxx) 发送MQ
    }
}

再过了一段时间,业务方觉得市场反应更好,于是希望对业务和场景进行细分:

事业部:业务类型除了原来的专车、快车,还要增加出租车和拼车,其中专车又分舒适型、豪华型、商务型,快车有普通用车、接送机、企业用车的场景……
产品经理:没问题,我设计一下,保证您满意。

产品经理针对状态机和业务流程进行了重新的设计和梳理,在和研发做需求评审的时候,研发沉不住气了:

研发:哇靠,不早说有这种需求,早说我们就设计的更好一点。这个需求现在的代码压根没法支持,除非重构,不然没有办法实现;
产品经理:再想一下有没有更简单的方法呢?
研发:没有
产品经理:好吧……(一口老血喷出来)

如果没有设计好,这种人为的复杂性导致系统越来越臃肿,越来越难维护,酱缸的老代码发出一阵阵恶臭,新来的同学,往往要捂着鼻子抠几天甚至几个月,才能理清系统和业务脉络,然后又一头扎进各种bug fix,业务修补的恶性循环中,暗无天日!
暗无天日的bug fix

业务的发展过程中,唯一不变的就是变化。如果快速支撑业务,就要寻找出不变的点,以不变应万变,这才是上上策。

COLA介绍

状态机

什么是状态机

状态机是有限状态自动机的简称,表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个LED等,就有亮和灭两种状态。我们通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如LED灯的状态就是两个亮和灭。

状态机一般包含以下四个要素:

  • State:状态。一个标准的状态机最少包含两个状态:初始和终态。
  • Event:事件。还有中描述叫Trigger,表达的意思都一样,就是要执行某个操作的触发器或口令:当状态机处于某个状态时,只有外界告诉状态机要干什么事情的时候,状态机才会去执行具体的行为,来完成外界想要它完成的操作。
  • Action:行为。状态变更索要执行的具体行为。
  • Transition:变更。一个状态接收一个事件执行了某些行为到达了另外一个状态的过程就是一个Transition。定义Transition就是在定义状态机的运转流程。

类比:吃饭的,我们喊一声“点菜”,服务员就会拿着本子过来记录要点什么菜,点完之后会从待下单状态改为已下单状态。待下单、已下单就是State,点菜就是Event,服务员拿本子过来记录就是Action,整个过程的运转就是一个Transition。

状态机有什么用

状态机可以让程序的结构更加清晰,可读性也会更高。通过编排的方式将状态、事件和对应的处理逻辑定义好,让整个流程更加规整、有序。

COLA状态机实现

  • State:状态
  • Event:事件,状态由事件触发,引起变化
  • Transition:流转,表示从一个状态到另一个状态
  • External Transition:外部流转,两个不同状态之间的流转
  • Internal Transition:内部流转,同一个状态之间的流转
  • Condition:条件,表示是否允许到达某个状态
  • Action:动作,到达某个状态之后,可以做什么
  • StateMachine:状态机

Cola状态机

整个状态机的核心代码如下所示:

//StateMachine
public class StateMachineImpl<S,E,C> implements StateMachine<S, E, C> {

  private String machineId;
  private final Map<S, State<S,E,C>> stateMap;

  ...
}

  //State
  public class StateImpl<S,E,C> implements State<S,E,C> {
    protected final S stateId;
    private Map<E, Transition<S, E,C>> transitions = new HashMap<>();

  ...
}

  //Transition
  public class TransitionImpl<S,E,C> implements Transition<S,E,C> {

    private State<S, E, C> source;
    private State<S, E, C> target;
    private E event;
    private Condition<C> condition;
    private Action<S,E,C> action;

    ...
}

具体使用可以看单元测试类StateMachineTest.java,摘取如下:
状态机使用Demo

扩展节点

什么是扩展节点

扩展节点(Extension Point)本身是一个接口,是很多可扩展项目中一个关键的机制,可以利用扩展向平台添加新功能。按照扩展点(Extension Point)定义的规范进行实现的部分称为扩展

扩展节点有什么用

扩展节点提高了系统的扩展性,基于扩展节点,自己或者其他研发同学可以在不污染现有业务逻辑的情况下进行扩展,非常适合一块逻辑在不同的业务有不同的实现的场景。

COLA扩展节点实现

首先看下COLA中扩展节点注解的源码:

/**
 * Extension 
 * @author fulan.zjf 2017-11-05
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface Extension {
    String bizId()  default BizScenario.DEFAULT_BIZ_ID;
    String useCase() default BizScenario.DEFAULT_USE_CASE;
    String scenario() default BizScenario.DEFAULT_SCENARIO;
}
  • bizId:指某个具体的业务,例如淘宝、天猫、聚划算;
  • useCase:对应业务下的某个Case,例如登录,下单;
  • Scenario:对应的场景,例如登录场景下有微信登陆、小程序登录、手机号登录等。
  • 该方式实现扩展节点很好的对应了测试用例(也是Case和Scenario)。下图更直观的表达了扩展节点对应的含义:
    Extension场景说明

在COLA中,要实现扩展节点只需要定义扩展节点(需要以ExtPt结尾),然后进行实现即可,如下:

// 定义扩展节点,扩展节点名称需要以ExtPt结尾
public interface OrderExtPt<C extends Context> extends ExtensionPointI {
    /**
     * 处理订单
     *
     * @param context 上下文信息
     */
    void process(C context);
}

// 扩展节点实现
@Extension(useCase = UseCase.CREATE_ORDER, scenario = Scenario.PREPAY)
public class PrepayCreateOrderExtPt implements OrderExtPt<OrderContext> {

    @Override
    public void process(OrderContext context) {
        System.out.println("current processor: " + this.getClass().getName());
    }
}

关于COLA扩展节点的核心实现代码摘取如下:
Cola扩展节点查找过程
COLA优先根据bizId + useCase + scenario查找扩展节点实现,如果找到返回并执行;如果找不到继续根据bizId + useCase查找,再找不到根据bizId进行查找。这个过程本身做了降级的兼容。

重构打车业务

了解完COLA的状态机和扩展节点,我们一起来看下怎么重构打车的订单业务吧!

需要注意的是,不管是用COLA还是自己实现状态机和扩展节点,很重要的一环还是要对业务做好充分的分析设计,如果没设计好,再好的框架还是会出现一系列问题。

根据前面梳理的状态机的几个要素:State、Event、Action、Transition,我们要根据业务重新梳理下,其中State部分产品已经梳理完成,可以直接使用;Event部分可以基于几个发生的重要节点来定义:下单、派单、支付、完成、取消来,对应的Action由于在不同的业务类型、场景下会有不同的实现,可以考虑扩展节点来实现。

下面通过简单的代码来演示下:

// 初始化状态机
@Component
public class StateMachineInitializer {

    @Resource
    private ExtensionExecutor extensionExecutor;

    /**
     * 初始化主订单状态机
     */
    @PostConstruct
    public void initOrderStateMachine() {
        StateMachineBuilder<OrderStatus, OrderEvent, OrderContext> builder = StateMachineBuilderFactory.create();

        // 订单创建,condition看实际条件确定是否设置
        builder.internalTransition().within(OrderStatus.CREATED).on(OrderEvent.CREATE).when((ctx) -> true)
            .perform(processOrder(UseCase.CREATE_ORDER));

        // 订单由创建状态变为支付状态,condition看实际条件确定是否设置
        builder.externalTransition().from(OrderStatus.CREATED).to(OrderStatus.PAID).on(OrderEvent.PAY)
            .when((ctx) -> true).perform(processOrder(UseCase.PAY));

        // 取消
        builder.externalTransition().from(OrderStatus.CREATED).to(OrderStatus.CANCELED).on(OrderEvent.CANCEL)
            .when((ctx) -> true).perform(processOrder(UseCase.CANCEL));

        builder.build(StateMachines.ORDER_STATE_MACHINE);
    }

    private Action<OrderStatus, OrderEvent, OrderContext> processOrder(String useCase) {
        return (from, to, event, ctx) -> {
            BizScenario bizScenario = BizScenario.valueOf(BizScenario.DEFAULT_BIZ_ID, useCase, ctx.getOrderType());
            extensionExecutor.executeVoid(OrderExtPt.class, bizScenario, extension -> extension.process(ctx));
        };
    }
}

// 定义订单扩展节点
public interface OrderExtPt<C extends Context> extends ExtensionPointI {
    /**
     * 处理订单
     *
     * @param context 上下文信息
     */
    void process(C context);
}

/**
 * 快车下单,机场接机场景
 *
 * @author: Eric
 * @date: 2021/8/25 11:10 下午
 * @since: 1.0.0
 */
@Extension(bizId = "express", useCase = "placeOrder", scenario="airport")
public class ExpressCreateOrderExtPt implements OrderExtPt<OrderContext> {

    @Override
    public void process(OrderContext context) {
        System.out.println("current processor: " + this.getClass().getName());
    }
}

/**
 * 专车下单, 所有的接单通用
 *
 * @author: Eric
 * @date: 2021/8/25 11:10 下午
 * @since: 1.0.0
 */
@Extension(bizId = "tailored", useCase = "placeOrder")
public class TailoredCarCreateOrderExtPt implements OrderExtPt<OrderContext> {

    @Override
    public void process(OrderContext context) {
        System.out.println("current processor: " + this.getClass().getName());
    }
}

/**
 * 快车取消订单
 *
 * @author: Eric
 * @date: 2021/8/25 11:10 下午
 * @since: 1.0.0
 */
@Extension(bizId = "express", useCase = "cancelOrder")
public class ExpressCancelOrderExtPt implements OrderExtPt<OrderContext> {

    @Override
    public void process(OrderContext context) {
        System.out.println("current processor: " + this.getClass().getName());
    }
}

/**
 * 专车取消订单
 *
 * @author: Eric
 * @date: 2021/8/25 11:10 下午
 * @since: 1.0.0
 */
@Extension(bizId = "tailored", useCase = "cancelOrder")
public class TailoredCarCancelExtPt implements OrderExtPt<OrderContext> {

    @Override
    public void process(OrderContext context) {
        System.out.println("current processor: " + this.getClass().getName());
    }
}

重构完,就像买了个好慷的收纳服务,收拾的规规整整

未来如果还有新业务或者新场景,只需要增加一个新的扩展节点实现就可以了,对现有逻辑不影响

研发:还要增加新的打车类型或者场景吗?我们可以很快实现。
产品经理:我问一下事业部要不要加
事业部:暂时先不用,我们先运营,有需要再提需求哈。

参考资料


文章作者: zzq0324
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zzq0324 !
  目录