重學 Java 設計模式:實戰責任鏈模式「模擬618電商大促期間,專案上線流程多級負責人審批場景」

小傅哥發表於2020-06-19


作者:小傅哥
部落格:https://bugstack.cn - 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

場地和場景的重要性

射擊?需要去靶場學習、滑雪?需要去雪場體驗、開車?需要能上路實踐,而程式設計開發除了能完成產品的功能流程,還需要保證系統的可靠效能。就像你能聽到的一些系統監控指標;QPSTPSTP99TP999可用率響應時長等等,而這些指標的總和評估就是一個系統的健康度。但如果你幾乎沒有聽到這樣的技術術語,也沒接觸過類似高併發場景,那麼就很像駕駛證的科目1考了100分,但不能上路。沒有這樣的技術場景給你訓練,讓你不斷的體會系統的脾氣秉性,即便你有再多的想法都沒法實現。所以,如果真的想學習一定要去一個有實操的場景,下水試試才能學會狗刨。

你的視覺盲區有多大

同樣一本書、同樣一條路、同樣一座城,你真的以為生活有選擇嗎?有時候很多選項都是擺設,給你多少次機會你都選的一模一樣。這不是你選不選而是你的認知範圍決定了你下一秒做的事情,另外的一個下一秒又決定了再下一個下一秒。就像管中窺豹一樣,20%的面積在你視覺裡都是黑色的,甚至就總是忽略看不到,而這看不到的20%就是生命中的時運!但,人可以學習,可以成長,可以脫胎換骨,可以努力付出,通過一次次的蛻變而看到剩下的20%!

沒有設計圖紙你敢蓋樓嗎

程式設計開發中最好的什麼,是設計。運用架構思維、經驗心得、才華靈感,構建出最佳的系統。真正的研發會把自己寫的程式碼當做作品來欣賞,你說這是一份工作,但在這樣的人眼裡這可不是一份工作,而是一份工匠精神。就像可能時而你也會為自己因為一個niubility的設計而豪邁萬丈,為能上線一個扛得住每秒200萬訪問量的系統會精神煥發。這樣的自豪感就是一次次壘磚一樣墊高腳底,不斷的把你的視野提高,讓你能看到上層設計也能知曉根基建設。可以把控全域性,也可以治理細節。這一份份知識的沉澱,來幫助你繪製出一張系統架構藍圖。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-13-00 場景模擬工程;模擬一個上線流程審批的介面。
itstack-demo-design-13-01 使用一坨程式碼實現業務需求
itstack-demo-design-13-02 通過設計模式優化改造程式碼,產生對比性從而學習

三、責任鏈模式介紹

責任鏈模式,圖片來自 refactoringguru.cn

擊鼓傳雷,看上圖你是否想起周星馳有一個電影,大家坐在海邊圍成一個圈,拿著一個點燃的炸彈,互相傳遞。

責任鏈模式的核心是解決一組服務中的先後執行處理關係,就有點像你沒錢花了,需要家庭財務支出審批,10塊錢以下找閨女審批,100塊錢先閨女審批在媳婦審批。你可以理解想象成當你要跳槽的時候被安排的明明白白的被各個領導簽字放行。

四、案例場景模擬

場景模擬;618大促場景上線審批場景

在本案例中我們模擬在618大促期間的業務系統上線審批流程場景

像是這些一線電商類的網際網路公司,阿里、京東、拼多多等,在618期間都會做一些運營活動場景以及提供的擴容備戰,就像過年期間百度的紅包一樣。但是所有開發的這些系統都需要陸續的上線,因為臨近618有時候也有一些緊急的調整的需要上線,但為了保障線上系統的穩定性是儘可能的減少上線的,也會相應的增強審批力度。就像一級響應、二級響應一樣。

而這審批的過程在隨著特定時間點會增加不同級別的負責人加入,每個人就像責任鏈模式中的每一個核心點。對於研發小夥伴並不需要關心具體的審批流程處理細節,只需要知道這個上線更嚴格,級別也更高,但對於研發人員來說同樣是點選相同的提審按鈕,等待稽核。

接下來我們就模擬這樣一個業務訴求場景,使用責任鏈的設計模式來實現此功能。

1. 場景模擬工程

itstack-demo-design-13-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── AuthService.java
  • 這裡的程式碼結構比較簡單,只有一個模擬稽核和查詢稽核結果的服務類。相當於你可以呼叫這個類去稽核工程和獲取稽核結構,這部分結果資訊是模擬的寫到快取實現。

2. 場景簡述

2.1 模擬稽核服務

public class AuthService {

    private static Map<String, Date> authMap = new ConcurrentHashMap<String, Date>();

    public static Date queryAuthInfo(String uId, String orderId) {
        return authMap.get(uId.concat(orderId));
    }

    public static void auth(String uId, String orderId) {
        authMap.put(uId.concat(orderId), new Date());
    }

}
  • 這裡面提供了兩個介面一個是查詢稽核結果(queryAuthInfo)、另外一個是處理稽核(auth)。
  • 這部分是把由誰稽核的和稽核的單子ID作為唯一key值記錄到記憶體Map結構中。

五、用一坨坨程式碼實現

這裡我們先使用最直接的方式來實現功能

按照我們的需求審批流程,平常系統上線只需要三級負責人審批就可以,但是到了618大促時間點,就需要由二級負責以及一級負責人一起加入審批系統上線流程。在這裡我們使用非常直接的if判斷方式來實現這樣的需求。

1. 工程結構

itstack-demo-design-13-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── AuthController.java
  • 這部分非常簡單的只包含了一個稽核的控制類,就像有些夥伴開始寫程式碼一樣,一個類寫所有需求。

2. 程式碼實現

public class AuthController {

    private SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化

    public AuthInfo doAuth(String uId, String orderId, Date authDate) throws ParseException {

        // 三級審批
        Date date = AuthService.queryAuthInfo("1000013", orderId);
        if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待三級審批負責人 ", "王工");

        // 二級審批
        if (authDate.after(f.parse("2020-06-01 00:00:00")) && authDate.before(f.parse("2020-06-25 23:59:59"))) {
            date = AuthService.queryAuthInfo("1000012", orderId);
            if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待二級審批負責人 ", "張經理");
        }

        // 一級審批
        if (authDate.after(f.parse("2020-06-11 00:00:00")) && authDate.before(f.parse("2020-06-20 23:59:59"))) {
            date = AuthService.queryAuthInfo("1000011", orderId);
            if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待一級審批負責人 ", "段總");
        }

        return new AuthInfo("0001", "單號:", orderId, " 狀態:審批完成");
    }

}
  • 這裡從上到下分別判斷了在指定時間範圍內由不同的人員進行審批,就像618上線的時候需要三個負責人都審批才能讓系統進行上線。
  • 像是這樣的功能看起來很簡單的,但是實際的業務中會有很多部門,但如果這樣實現就很難進行擴充套件,並且在改動擴充套件調整也非常麻煩。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_AuthController() throws ParseException {
    AuthController authController = new AuthController();  

    // 模擬三級負責人審批
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬三級負責人審批,王工");
    AuthService.auth("1000013", "1000998004813441");  

    // 模擬二級負責人審批                                 
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬二級負責人審批,張經理");
    AuthService.auth("1000012", "1000998004813441");    

    // 模擬一級負責人審批
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬一級負責人審批,段總");
    AuthService.auth("1000011", "1000998004813441");            

    logger.info("測試結果:{}", "審批完成");
}
  • 這裡模擬每次查詢是否審批完成,隨著審批的不同節點,之後繼續由不同的負責人進行審批操作。
  • authController.doAuth,是檢視審批的流程節點、AuthService.auth,是審批方法用於操作節點流程狀態。

3.2 測試結果

23:25:00.363 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待三級審批負責人 王工"}
23:25:00.366 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬三級負責人審批,王工
23:25:00.367 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待二級審批負責人 張經理"}
23:25:00.367 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬二級負責人審批,張經理
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待一級審批負責人 段總"}
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬一級負責人審批,段總
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:審批完成

Process finished with exit code 0
  • 從測試結果上可以看到一層層的由不同的人員進行審批,審批完成後到下一個人進行處理。單看結果是滿足我們的訴求,只不過很難擴充套件和調整流程,相當於程式碼寫的死死的。

六、責任鏈模式重構程式碼

接下來使用裝飾器模式來進行程式碼優化,也算是一次很小的重構。

責任鏈模式可以讓各個服務模組更加清晰,而每一個模組間可以通過next的方式進行獲取。而每一個next是由繼承的統一抽象類實現的。最終所有類的職責可以動態的進行編排使用,編排的過程可以做成可配置化。

1. 工程結構

itstack-demo-design-13-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │    ├── Level1AuthLink.java
                │    ├── Level2AuthLink.java
                │    └── Level3AuthLink.java
                ├── AuthInfo.java
                └── AuthLink.java

責任鏈模式模型結構

責任鏈模式模型結構

  • 上圖是這個業務模型中責任鏈結構的核心部分,通過三個實現了統一抽象類AuthLink的不同規則,再進行責任編排模擬出一條鏈路。這個鏈路就是業務中的責任鏈。
  • 一般在使用責任鏈時候如果是場景比較固定,可以通過寫死到程式碼中進行初始化。但如果業務場景經常變化可以做成xml配置的方式進行處理,也可以落到庫裡進行初始化操作。

2. 程式碼實現

2.1 責任鏈中返回物件定義

public class AuthInfo {

    private String code;
    private String info = "";

    public AuthInfo(String code, String ...infos) {
        this.code = code;
        for (String str:infos){
            this.info = this.info.concat(str);
        }
    }
    
    // ...get/set
}
  • 這個類的是包裝了責任鏈處理過程中返回結果的類,方面處理每個責任鏈的返回資訊。

2.2 鏈路抽象類定義

public abstract class AuthLink {

    protected Logger logger = LoggerFactory.getLogger(AuthLink.class);

    protected SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化
    protected String levelUserId;                           // 級別人員ID
    protected String levelUserName;                         // 級別人員姓名
    private AuthLink next;                                  // 責任鏈

    public AuthLink(String levelUserId, String levelUserName) {
        this.levelUserId = levelUserId;
        this.levelUserName = levelUserName;
    }

    public AuthLink next() {
        return next;
    }

    public AuthLink appendNext(AuthLink next) {
        this.next = next;
        return this;
    }

    public abstract AuthInfo doAuth(String uId, String orderId, Date authDate);

}
  • 這部分是責任鏈,連結起來的核心部分。AuthLink next,重點在於可以通過next方式獲取下一個鏈路需要處理的節點。
  • levelUserIdlevelUserName,是責任鏈中的公用資訊,標記每一個稽核節點的人員資訊。
  • 抽象類中定義了一個抽象方法,abstract AuthInfo doAuth,這是每一個實現者必須實現的類,不同的稽核級別處理不同的業務。

2.3 三個稽核實現類

Level1AuthLink

public class Level1AuthLink extends AuthLink {

    public Level1AuthLink(String levelUserId, String levelUserName) {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待一級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:一級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}

Level2AuthLink

public class Level2AuthLink extends AuthLink {

    private Date beginDate = f.parse("2020-06-11 00:00:00");
    private Date endDate = f.parse("2020-06-20 23:59:59");

    public Level2AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待二級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:二級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:二級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}

Level3AuthLink

public class Level3AuthLink extends AuthLink {

    private Date beginDate = f.parse("2020-06-01 00:00:00");
    private Date endDate = f.parse("2020-06-25 23:59:59");

    public Level3AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待三級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:三級審批負責人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:三級審批負責人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}
  • 如上三個類;Level1AuthLinkLevel2AuthLinkLevel3AuthLink,實現了不同的稽核級別處理的簡單邏輯。
  • 例如第一個稽核類中會先判斷是否稽核通過,如果沒有稽核通過則返回結果給呼叫方,引導去稽核。(這裡簡單模擬稽核後有時間資訊不為空,作為判斷條件)
  • 判斷完成後獲取下一個稽核節點;super.next();,如果不存在下一個節點,則直接返回結果。
  • 之後是根據不同的業務時間段進行判斷是否需要,二級和一級的稽核。
  • 最後返回下一個稽核結果;next.doAuth(uId, orderId, authDate);,有點像遞迴呼叫。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_AuthLink() throws ParseException {
    AuthLink authLink = new Level3AuthLink("1000013", "王工")
            .appendNext(new Level2AuthLink("1000012", "張經理")
                    .appendNext(new Level1AuthLink("1000011", "段總")));

    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬三級負責人審批
    AuthService.auth("1000013", "1000998004813441");
    logger.info("測試結果:{}", "模擬三級負責人審批,王工");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬二級負責人審批
    AuthService.auth("1000012", "1000998004813441");
    logger.info("測試結果:{}", "模擬二級負責人審批,張經理");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬一級負責人審批
    AuthService.auth("1000011", "1000998004813441");
    logger.info("測試結果:{}", "模擬一級負責人審批,段總");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));
}
  • 這裡包括最核心的責任鏈建立,實際的業務中會包裝到控制層; AuthLink authLink = new Level3AuthLink("1000013", "王工") .appendNext(new Level2AuthLink("1000012", "張經理") .appendNext(new Level1AuthLink("1000011", "段總"))); 通過把不同的責任節點進行組裝,構成一條完整業務的責任鏈。
  • 接下里不斷的執行檢視稽核鏈路authLink.doAuth(...),通過返回結果對資料進行3、2、1級負責人稽核,直至最後稽核全部完成。

3.2 測試結果

23:49:46.585 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待三級審批負責人 王工"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬三級負責人審批,王工
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待二級審批負責人 張經理"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬二級負責人審批,張經理
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待一級審批負責人 段總"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬一級負責人審批,段總
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0000","info":"單號:1000998004813441 狀態:一級審批完成負責人 時間:2020-06-18 23:49:46 審批人:段總"}

Process finished with exit code 0
  • 從上述的結果可以看到我們的責任鏈已經生效,按照責任鏈的結構一層層審批,直至最後輸出審批結束到一級完成的結果。
  • 這樣責任鏈的設計方式可以方便的進行擴充套件和維護,也把if語句幹掉了。

七、總結

  • 從上面程式碼從if語句重構到使用責任鏈模式開發可以看到,我們的程式碼結構變得清晰乾淨了,也解決了大量if語句的使用。並不是if語句不好,只不過if語句並不適合做系統流程設計,但是在做判斷和行為邏輯處理中還是非常可以使用的。
  • 在我們前面學習結構性模式中講到過組合模式,它像是一顆組合樹一樣,我們搭建出一個流程決策樹。其實這樣的模式也是可以和責任鏈模型進行組合擴充套件使用,而這部分的重點在於如何關聯鏈路的關聯,最終的執行都是在執行在中間的關係鏈。
  • 責任鏈模式很好的處理單一職責和開閉原則,簡單了耦合也使物件關係更加清晰,而且外部的呼叫方並不需要關心責任鏈是如何進行處理的(以上程式中可以把責任鏈的組合進行包裝,在提供給外部使用)。但除了這些優點外也需要是適當的場景才進行使用,避免造成效能以及編排混亂除錯測試疏漏問題。

八、推薦閱讀

相關文章