利用狀態(tài)機(jī)處理訂單狀態(tài)流轉(zhuǎn)

首先簡述一個(gè)場景要拂,訂單的狀態(tài)流轉(zhuǎn)。 一個(gè)訂單會(huì)有很多種狀態(tài):臨時(shí)單站楚、已下單脱惰、待支付、已支付窿春、已完成拉一、退款中等等采盒。每一種狀態(tài)都和其扭轉(zhuǎn)前的狀態(tài)、在扭轉(zhuǎn)前狀態(tài)所執(zhí)行的操作有關(guān)舅踪。

一 引子

舉例一個(gè)過程:用戶將商品加入購物車纽甘,在后臺(tái)生成了一個(gè)所謂的“臨時(shí)單”,這個(gè)訂單實(shí)際上還沒有正式生成抽碌,因?yàn)橛脩羧匀粵]有點(diǎn)擊下單悍赢。只有當(dāng)用戶下單后,這個(gè)“臨時(shí)單”才可以轉(zhuǎn)化為一個(gè)“待支付的訂單”货徙。那么這個(gè)過程中:只有將一個(gè)處于“臨時(shí)單”狀態(tài)的訂單執(zhí)行下單操作左权,才能得到一個(gè)狀態(tài)為“待支付”的訂單。 即--一個(gè)前置狀態(tài)+一個(gè)恰當(dāng)?shù)牟僮鞒占眨拍芘まD(zhuǎn)訂單的狀態(tài)赏迟。在這個(gè)過程中,如果是硬編碼蠢棱,那么我們需要一系列的 if...else 語句來檢查訂單的當(dāng)前狀態(tài)锌杀、可執(zhí)行操作以及這兩個(gè)的組合得到的下一個(gè)應(yīng)該被流轉(zhuǎn)的狀態(tài)值。如果訂單的狀態(tài)流轉(zhuǎn)很復(fù)雜的話泻仙,寫出來的邏輯就會(huì)很復(fù)雜糕再,并且可讀性很低。后期的維護(hù)就是一個(gè)坑玉转。

二 狀態(tài)設(shè)計(jì)模式與訂單狀態(tài)流轉(zhuǎn)

處理這個(gè)問題突想,我們可以使用 狀態(tài)機(jī)設(shè)計(jì)模式 來處理。對(duì)應(yīng)到實(shí)踐究抓,就是狀態(tài)機(jī)猾担。

關(guān)于狀態(tài)機(jī)設(shè)計(jì)模式的具體內(nèi)容,可以自行百度刺下。這里用簡單的一句話來概括的話:對(duì)象的內(nèi)部狀態(tài)隨外部執(zhí)行條件的變化而變化绑嘹。再映射到訂單狀態(tài)的流轉(zhuǎn)上:訂單的狀態(tài),隨訂單當(dāng)前狀態(tài)和目前執(zhí)行操作的組合而變化橘茉。

三 編碼前的抽象與設(shè)計(jì)

state.jpg

圖示模擬一個(gè)訂單狀態(tài)的流轉(zhuǎn)流程圾叼。從一個(gè)臨時(shí)訂單開始,每當(dāng)訂單處于某一個(gè)已知的狀態(tài)的時(shí)候捺癞,要想讓這個(gè)訂單改變狀態(tài),就需要我們?nèi)?zhí)行對(duì)應(yīng)的操作构挤。

從狀態(tài)機(jī)角度來說髓介,我們先將各種信息進(jìn)行抽象和處理

3.1 代碼抽象

編寫對(duì)應(yīng)訂單狀態(tài)枚舉類

public enum OrderStatusEnum {

    CREATE_EVENT(1, "創(chuàng)建訂單"),
    FORMAL_EVENT(2, "正式訂單"),
    NEED_PAY(3, "待支付"),
    PAY_DONE(4, "已支付"),
    ORDER_FINISHED(5, "訂單已完成"),

    ORDER_CANCEL(6, "訂單已取消");

    OrderStatusEnum(int status, String desc) {
        this.status = status;
        this.desc = desc;
    }

    public int status;

    public String desc;
}

枚舉類中先準(zhǔn)備好需要用的狀態(tài)信息。

先用一張圖來描述整個(gè)工作機(jī)制:

description.jpg

然后是需要的核心代碼部分:一個(gè)管理訂單狀態(tài)的中轉(zhuǎn)站manager類筋现,一組用于扭轉(zhuǎn)訂單狀態(tài)的operator類唐础,一組扭轉(zhuǎn)完訂單狀態(tài)后執(zhí)行后續(xù)邏輯操作的processor類箱歧。

manager類需要根據(jù)對(duì)應(yīng)傳入的當(dāng)前訂單狀態(tài)、要對(duì)該訂單執(zhí)行操作來得到這個(gè)訂單的結(jié)果狀態(tài)(依靠對(duì)應(yīng)的opertor類)一膨,然后執(zhí)行一系列需要的業(yè)務(wù)邏輯操作(編寫對(duì)應(yīng)的processor類)呀邢。這樣的好處就是將訂單狀態(tài)流轉(zhuǎn)和對(duì)應(yīng)的業(yè)務(wù)處理解耦。并且也不會(huì)再有一堆繁雜的 if...else 操作豹绪。每當(dāng)需要新的訂單狀態(tài)流轉(zhuǎn)操作的時(shí)候价淌,可以去編寫對(duì)應(yīng)的一套o(hù)perator和processor組件來完成,和已有業(yè)務(wù)的分離度很高瞒津。

接下來貼代碼舉例

/**
 * 訂單狀態(tài)流轉(zhuǎn)管理器--狀態(tài)機(jī)核心組件
 * @author carpeng.gao@qunar.com
 * @date 2018/10/29 19:21
 **/
@Component
public class OrderStateManager {

    Map<Integer, AbstractOrderOperator> orderOperatorMaps = new HashMap<Integer, AbstractOrderOperator>();

    Map<Integer, AbstractOrderProcessor> orderProcessorMaps = new HashMap<Integer, AbstractOrderProcessor>();

    public OrderStateManager() { }

    /**
     * 狀態(tài)流轉(zhuǎn)方法
     * @param orderId 訂單id
     * @param event 流轉(zhuǎn)的訂單操作事件
     * @param status 當(dāng)前訂單狀態(tài)
     * @return 扭轉(zhuǎn)后的訂單狀態(tài)
     */
    public int handleEvent(final String orderId, OrderStatusEnum event, final int status) {
        if (this.isFinalStatus(status)) {
            throw new IllegalArgumentException("handle event can't process final state order.");
        }
        // 獲取對(duì)應(yīng)處理器,根據(jù)入?yún)顟B(tài)和時(shí)間獲取訂單流轉(zhuǎn)的結(jié)果狀態(tài)
        AbstractOrderOperator abstractOrderOperator = this.getStateOperator(event);
        int resState = abstractOrderOperator.handleEvent(status, event);
        // 得到結(jié)果狀態(tài)蝉衣,在對(duì)應(yīng)的processor中處理訂單數(shù)據(jù)及其相關(guān)信息
        AbstractOrderProcessor orderProcessor = this.getOrderProcessor(event);
        if (!orderProcessor.process(orderId, resState)) {
            throw new IllegalStateException(String.format("訂單狀態(tài)流轉(zhuǎn)失敗,訂單id:%s", orderId));
        }
        return resState;
    }

    /**
     * 根據(jù)入?yún)顟B(tài)枚舉實(shí)例獲取對(duì)應(yīng)的狀態(tài)處理器
     * @param event event
     * @return
     */
    private AbstractOrderOperator getStateOperator(OrderStatusEnum event) {
        AbstractOrderOperator operator = null;
        for (Map.Entry<Integer, AbstractOrderOperator> entry: orderOperatorMaps.entrySet()) {
            if (event.status == entry.getKey()) {
                operator = entry.getValue();
            }
        }
        if (null == operator) {
            throw new IllegalArgumentException(String.format("can't find proper operator. The parameter state :%s", event.toString()));
        }
        return operator;
    }

    /**
     * 根據(jù)入?yún)顟B(tài)枚舉實(shí)例獲取對(duì)應(yīng)的狀態(tài)后處理器
     * @param event event
     * @return
     */
    private AbstractOrderProcessor getOrderProcessor(OrderStatusEnum event) {
        AbstractOrderProcessor processor = null;
        for (Map.Entry<Integer, AbstractOrderProcessor> entry : orderProcessorMaps.entrySet()) {
            if (event.status == entry.getKey()) {
                processor = entry.getValue();
            }
        }
        if (null == processor) {
            throw new IllegalArgumentException(String.format("can't find proper processor. The parameter state :%s", event.toString()));
        }
        return processor;
    }


    /**
     * 判斷是不是已完成訂單
     * @param status 訂單狀態(tài)碼
     * @return
     */
    private boolean isFinalStatus(int status) {
        return OrderStatusEnum.ORDER_FINISHED.status == status;
    }

}

核心的代碼就是類中的 handleEvent 方法巷蚪。

對(duì)應(yīng)的獲取到的組件處理類示例:

/**
 * @author carpeng.gao@qunar.com
 * @date 2018/10/29 17:47
 **/
@Data
public abstract class AbstractOrderOperator {

    int status;

    public abstract int handleEvent(int orderStatus, OrderStatusEnum orderStatusEnum);
}
===================================
/**
 * 創(chuàng)建訂單操作狀態(tài)流轉(zhuǎn)
 * @author carpeng.gao@qunar.com
 * @date 2018/10/29 17:50
 **/
@Component
@OrderOperator
public class CreateOrderOperator extends AbstractOrderOperator {

    public CreateOrderOperator() {
        super.setStatus(1);
    }

    @Override
    public int handleEvent(int orderStatus, OrderStatusEnum orderStatusEnum) {
        if (orderStatus != OrderStatusEnum.CREATE_EVENT.status && orderStatus != OrderStatusEnum.ORDER_CANCEL.status) {
            throw new IllegalArgumentException(String.format("create operation can't handle the status: %s", orderStatus));
        }
        System.out.println("進(jìn)入創(chuàng)建訂單狀態(tài)扭轉(zhuǎn)處理器...");
        switch (orderStatusEnum) {
            case CREATE_EVENT:
                return OrderStatusEnum.FORMAL_EVENT.status;
            case ORDER_CANCEL:
                return OrderStatusEnum.ORDER_CANCEL.status;
            default:
                return getStatus();
        }
    }
}

后處理器

/**
 * 訂單處理器
 * @author carpeng.gao@qunar.com
 * @date 2018/10/31 14:57
 **/
@Data
public abstract class AbstractOrderProcessor {

    int status;

    public abstract boolean process(String orderId, Object... params);
}
================================
/**
 * @author carpeng.gao@qunar.com
 * @date 2018/10/31 14:59
 **/
@Component
@OrderProcessor
public class CreateOrderProcessor extends AbstractOrderProcessor{

    public CreateOrderProcessor() {
        super.setStatus(1);
    }

    @Override
    public boolean process(String orderId, Object... params) {
        // TODO 創(chuàng)建/取消訂單對(duì)應(yīng)的數(shù)據(jù)庫修改病毡,mq發(fā)送等操作,可以在此處process方法中完成
        System.out.println("進(jìn)入創(chuàng)建訂單后處理器...");
        return true;
    }
}

這些組件類都是依賴于spring的組件掃描注入屁柏。如果要定制化地處理自己的組件類啦膜。可以用一些其他的技巧來處理淌喻。比如此處使用到了自定義注解僧家,通過自定義注解+自定義狀態(tài)機(jī)初始化類來完成對(duì)應(yīng)組件類的篩選與初始化。將這個(gè)初始化類加載完畢似嗤,狀態(tài)機(jī)就可以正常使用了啸臀。

/**
 * 狀態(tài)機(jī)前置激活類,在spring中掃描配置此類 <br/>
 * 使用自定義注解標(biāo)記對(duì)應(yīng)的狀態(tài)處理器和后置處理器并在初始化操作中完成對(duì)應(yīng)處理器的初始化。
 * @author carpeng.gao@qunar.com
 * @date 2018/10/31 15:30
 **/
@Component
public class Initialization implements BeanPostProcessor {

    @Resource
    OrderStateManager manager;

    @Nullable
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof AbstractOrderOperator && bean.getClass().isAnnotationPresent(OrderOperator.class) ) {
            AbstractOrderOperator orderState = (AbstractOrderOperator) bean;
            manager.orderOperatorMaps.put(orderState.getStatus(), orderState);
        }
        if (bean instanceof AbstractOrderProcessor && bean.getClass().isAnnotationPresent(OrderProcessor.class) ) {
            AbstractOrderProcessor orderProcessor = (AbstractOrderProcessor) bean;
            manager.orderProcessorMaps.put(orderProcessor.getStatus(), orderProcessor);
        }
        return bean;
    }
}

這里有一個(gè)問題就是在正式開發(fā)環(huán)境中烁落,依賴于項(xiàng)目的spring環(huán)境乘粒,需要在狀態(tài)機(jī)正式運(yùn)行前將對(duì)應(yīng)的狀態(tài)扭轉(zhuǎn)組件類(operator和processor)注入到環(huán)境中。

四 示例

寫完公司的項(xiàng)目后伤塌,自己再動(dòng)手編寫了一個(gè)簡化版的demo灯萍。包含了上述代碼和測試用例。

訂單狀態(tài)機(jī)demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末每聪,一起剝皮案震驚了整個(gè)濱河市旦棉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌药薯,老刑警劉巖绑洛,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異童本,居然都是意外死亡真屯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門穷娱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绑蔫,“玉大人,你說我怎么就攤上這事配深⌒恚” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵篓叶,是天一觀的道長烈掠。 經(jīng)常有香客問我,道長澜共,這世上最難降的妖魔是什么向叉? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮嗦董,結(jié)果婚禮上母谎,老公的妹妹穿的比我還像新娘。我一直安慰自己京革,他們只是感情好奇唤,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著匹摇,像睡著了一般咬扇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上廊勃,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天懈贺,我揣著相機(jī)與錄音,去河邊找鬼坡垫。 笑死梭灿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冰悠。 我是一名探鬼主播堡妒,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溉卓!你這毒婦竟也來了皮迟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤桑寨,失蹤者是張志新(化名)和其女友劉穎伏尼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尉尾,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烦粒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扰她。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖芭碍,靈堂內(nèi)的尸體忽然破棺而出徒役,到底是詐尸還是另有隱情,我是刑警寧澤窖壕,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布忧勿,位于F島的核電站,受9級(jí)特大地震影響瞻讽,放射性物質(zhì)發(fā)生泄漏鸳吸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一速勇、第九天 我趴在偏房一處隱蔽的房頂上張望晌砾。 院中可真熱鬧,春花似錦烦磁、人聲如沸养匈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呕乎。三九已至,卻和暖如春陨晶,著一層夾襖步出監(jiān)牢的瞬間猬仁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工先誉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留湿刽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓谆膳,卻偏偏與公主長得像叭爱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子漱病,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348