當前位置: 妍妍網 > 碼農

復雜業務下,如何優雅的使用設計模式來最佳化程式碼?

2024-04-02碼農

來自: juejin.cn/post/7291937079875928103

1、引言

本文以一個實際案例來介紹在解決業務需求的路上,如何透過常用的設計模式來逐級最佳化我們的程式碼,以把我們所了解的到設計模式真實的套用於實戰。

2、背景

假定我們現在有一個訂單流程管理系統,這個系統對於使用者發起的一筆訂單,需要你編寫程式碼按照以下環節進行依次處理

註:本文不會對每個環節的實作細節進行描述,讀者也不必了解這每個環節的實作,我們只需要關註程式碼架構設計

3、第一次叠代

按照背景,我們如果不是打算if-else一擼到底的話,我們最合適使用的設計模式應該是責任鏈模式,於是我們先打算用責任鏈模式來做我們的第一次叠代。

先整體看下類圖:

我們定義一個抽象類,抽象類中定義了下一個處理器,方便後期我們讀取配置直接構建責任鏈處理順序:

@Data
publicabstract classBizOrderHandler{
/**
* 下一個處理器
*/

private BizOrderHandler nextBizOrderHandler;
/**
* 處理器執行方法
@param param 責任鏈參數
@return 執行結果
*/

publicabstract Result handle(ProductVO param);

/**
* 鏈路傳遞
@param param
@return
*/

protected Result next(ProductVO param){
//下一個鏈路沒有處理器了,直接返回
if (Objects.isNull(nextBizOrderHandler)) {
return Result.success();
}
//執行下一個處理器
return nextBizOrderHandler.handle(param);
}
}


然後我們將需要實作的流程都來實作這個介面 (為了簡單只列舉一個)

public classStorageCheckBizOrderHandlerextendsBizOrderHandler{
@Override
public Result handle(ProductVO param){
// 這裏寫上倉儲管理的業務邏輯
System.out.println("StorageCheckBizOrderHandler doing business!");
returnsuper.next(param);
}
}

透過呼叫父類的next方法實作了鏈式傳遞,接下來我們就可以使用責任鏈來實作業務了

public classOrderHandleCases{
static Map<String, BizOrderHandler> handlerMap = new HashMap<>();
static {
handlerMap.put("Storage"new StorageCheckBizOrderHandler());
handlerMap.put("Payment"new PaymentBizOrderHandler());
handlerMap.put("RightCenter"new RightCenterBizOrderHandler());
handlerMap.put("PointDeduction"new PointDeductBizOrderHandler());
}
publicstaticvoidmain(String[] args)
// 這裏可以從nacos配置中心讀取責任鏈配置
BizOrderHandler handler1 = initHandler(Lists.newArrayList("Storage""RightCenter""PointDeduction","Payment"));
// 虛擬化一個產品訂單
ProductVO productVO = ProductVO.builder().build();
Result result = handler1.handle(productVO);
System.out.println("訂單處理 成功");
}

/**
* 根據責任鏈配置構建責任鏈
@param handlerNameChain 責任鏈執行順序
@return 第一個處理器
*/

privatestatic BizOrderHandler initHandler(List<String> handlerNameChain){
List<BizOrderHandler> handlers = new ArrayList<>();
for (int i = 0; i < handlerNameChain.size(); i++) {
String cur = handlerNameChain.get(i);
String next = i + 1 < handlerNameChain.size() ? handlerNameChain.get((i + 1)) : null;
BizOrderHandler handler = handlerMap.get(cur);
if (next != null) {
handler.setNextBizOrderHandler(handlerMap.get(next));
}
handlers.add(handler);
}
return handlers.get(0);
}
}



上面的程式碼中透過initHandler這個方法來組建整個責任鏈條,還能實作動態配置,比如後期需要撤掉積分模組的商品處理,改個配置就行,感覺責任鏈完美搞定了這個問題,第一版就這樣開心上線。

4、第二次叠代

產品又來了,提了一個新的需求

  • 產品說,我們需要支持多租戶,每種租戶的訂單流程都是不一樣的

  • 租戶A:倉儲檢查->權益扣減->積分扣減->剩余金額支付

  • 租戶B:倉儲檢查->積分扣減->權益扣減

  • 也就是說現在流程變成這樣:

    來了多租戶,這有何難,再加一條責任鏈配置不就好了,直接寫程式碼如下:

    public classOrderHandleCases{
    static Map<String, BizOrderHandler> handlerMap = new HashMap<>();
    static {
    handlerMap.put("Storage"new StorageCheckBizOrderHandler());
    handlerMap.put("Payment"new PaymentBizOrderHandler());
    handlerMap.put("RightCenter"new RightCenterBizOrderHandler());
    handlerMap.put("PointDeduction"new PointDeductBizOrderHandler());
    }
    publicstaticvoidmain(String[] args){
    // 租戶A的責任鏈
    BizOrderHandler handler1 = initHandler(Lists.newArrayList("Storage""RightCenter""PointDeduction""Payment"));
    ProductVO productVO = ProductVO.builder().build();
    Result result1 = handler1.handle(productVO);
    // 租戶B的責任鏈
    BizOrderHandler handler2 = initHandler(Lists.newArrayList("Storage""PointDeduction""RightCenter"));
    Result result2 = handler2.handle(productVO);
    System.out.println("訂單處理 成功");
    }
    /**
    * 根據責任鏈配置構建責任鏈
    @param handlerNameChain 責任鏈執行順序
    @return 第一個處理器
    */

    privatestatic BizOrderHandler initHandler(List<String> handlerNameChain){
    List<BizOrderHandler> handlers = new ArrayList<>();
    for (int i = 0; i < handlerNameChain.size(); i++) {
    String cur = handlerNameChain.get(i);
    String next = i + 1 < handlerNameChain.size() ? handlerNameChain.get((i + 1)) : null;
    BizOrderHandler handler = handlerMap.get(cur);
    if (next != null) {
    handler.setNextBizOrderHandler(handlerMap.get(next));
    }
    handlers.add(handler);
    }
    return handlers.get(0);
    }
    }




    上面的程式碼相比之前的就是多加了一條新的租戶B的責任鏈配置。感覺這個功能不就實作了嘛 結果一執行:堆疊溢位!

    咋回事 怎麽堆疊溢位了,咱們仔細看一下 發現咱們的Map裏面存放的例項全部是單例,搞出來了環形連結串列了....

    上圖中黑色箭頭代表第一個租戶的流程,綠色箭頭代表第二個租戶,第二個租戶的流程在執行到權益扣減環節,後面由於第一個租戶配置的下一個環節是積分扣減,於是在這裏形成了環。

    看來單例不行,咱們得搞多例 既然需要多次構建物件,於是咱們搬出來下一個設計模式「簡單工廠模式」:

    public classBizOrderHandlerFactory{
    publicstatic BizOrderHandler buildBizOrderHandler(String bizType){
    switch (bizType) {
    case"Storage":
    returnnew StorageCheckBizOrderHandler();
    case"Payment":
    returnnew PaymentBizOrderHandler();
    case"RightCenter":
    returnnew RightCenterBizOrderHandler();
    case"PointDeduction":
    returnnew PointDeductBizOrderHandler();
    default:
    returnnull;
    }
    }
    }

    然後我們覆寫initHandler方法,不再從map中取例項,轉為從工廠方法裏面獲取例項:

    privatestatic BizOrderHandler initHandlerPro(List<String> handlerNameChain){
    List<BizOrderHandler> handlers = new ArrayList<>();
    for (String s : handlerNameChain) {
    BizOrderHandler handler = BizOrderHandlerFactory.buildBizOrderHandler(s);
    handlers.add(handler);
    }
    for (int i = 0; i < handlers.size(); i++) {
    BizOrderHandler handler = handlers.get(i);
    BizOrderHandler nextHandler = i + 1 < handlerNameChain.size() ? handlers.get(i + 1) : null;
    handler.setNextBizOrderHandler(nextHandler);
    }
    return handlers.get(0);
    }
    publicstaticvoidmain(String[] args){
    BizOrderHandler handler1 = initHandlerPro(Lists.newArrayList("Storage""Payment""RightCenter""PointDeduction"));
    BizOrderHandler handler2 = initHandlerPro(Lists.newArrayList("Storage""RightCenter""PointDeduction"));
    ProductVO productVO = ProductVO.builder().build();
    Result result = handler1.handle(productVO);
    System.out.println("訂單處理 成功--租戶1");
    result = handler2.handle(productVO);
    System.out.println("訂單處理 成功--租戶2");
    }

    執行程式碼:

    好了問題完美解決,現在多租戶也支持了。上線搞定這次需求

    5、第三次叠代

    產品又又來了,提了一個新的需求

    產品說,我們需要支持條件判斷,租戶A要求,權益扣減和積分扣減必須全部成功完成一個就可以進入支付環節,不必都要把權益扣減和積分扣減流程走一遍

    分析一下這個需求權益扣減和積分扣減都要完成才可以進入支付環節 當然最簡單的改法是在權益和積分環節做個判斷,要是失敗了就跳出責任鏈,但是假如產品經理下次又說權益扣減和積分扣減完成一個就能進入支付,我們還得修改這個權益和積分實作裏面的判斷,頻繁修改實作可並不是好事。

    那咱們可以考慮代理模式,熟悉閘道器的都知道閘道器其實就是一個大代理,咱們按照這種思想可以搞一個閘道器代理權益扣減和積分扣減環節。於是咱們搞出來一個「閘道器」元件

    @Data
    public classBizOrderHandlerUnionGatewayextendsBizOrderHandler{
    List<BizOrderHandler> proxyHandlers;
    @Override
    public Result handle(ProductVO param){
    boolean isAllSuccess = true;
    if (proxyHandlers != null) {
    for (BizOrderHandler handler : proxyHandlers) {
    Result result = handler.handle(param);
    if (result.isSuccess()) {
    // 一個代理執行器 執行成功 則繼續執行
    continue;
    else {
    isAllSuccess = false;
    break;
    }
    }
    }
    if (isAllSuccess) {
    returnsuper.next(param);
    }else{
    thrownew RuntimeException("execute Failed");
    }
    }
    }

    上面的閘道器叫做union閘道器也就是聯集閘道器,也就是說代理的處理器全部都執行成功才繼續傳遞責任鏈,需要註意的是這個類也是 BizOrderHandler 的一個實作,只不過它的內部沒有邏輯,只是對 proxyHandlers 中的元件進行代理。

    然後簡單修改下工廠 加一個分支:

    publicstatic BizOrderHandler buildBizOrderHandler(String bizType){
    switch (bizType) {
    case"Storage":
    returnnew StorageCheckBizOrderHandler();
    case"Payment":
    returnnew PaymentBizOrderHandler();
    case"RightCenter":
    returnnew RightCenterBizOrderHandler();
    case"PointDeduction":
    returnnew PointDeductBizOrderHandler();
    case"UnionGateway":
    returnnew BizOrderHandlerUnionGateway();
    default:
    returnnull;
    }
    }

    然後我們用下面的方法獲取第一個執行節點,就可以執行整個責任鏈了:

    privatestatic BizOrderHandler initHandlerWithGateway(){
    BizOrderHandler storage = BizOrderHandlerFactory.buildBizOrderHandler("Storage");
    BizOrderHandler payment = BizOrderHandlerFactory.buildBizOrderHandler("Payment");
    BizOrderHandler rightCenter = BizOrderHandlerFactory.buildBizOrderHandler("RightCenter");
    BizOrderHandler pointDeduction = BizOrderHandlerFactory.buildBizOrderHandler("PointDeduction");
    BizOrderHandlerUnionGateway unionGateway = (BizOrderHandlerUnionGateway) BizOrderHandlerFactory.buildBizOrderHandler("UnionGateway");
    storage.setNextBizOrderHandler(unionGateway);
    unionGateway.setNextBizOrderHandler(payment);
    // unionGateway 加入責任鏈,權益和積分交給這個uniongateway進行代理控制
    unionGateway.setProxyHandlers(Lists.newArrayList(rightCenter, pointDeduction));
    return storage;
    }

    6、第四次叠代

    產品又又又來了,這次提了一個技術需求

    使用者反饋生產訂單流介面響應過慢,頁面卡頓,觀察介面發現目前的訂單流程需要走的鏈路比較冗長,雖然用了責任鏈模式但本質上程式碼執行仍然是同步的,導致一個訂單流完成耗費的時間過長,現在希望訂單流介面異步化,然後需要發揮分布式部署的優勢,每一個環節可以單獨分散到每個單個部署節點上執行。

    這次我們發現問題需要異步化還要分布式,這怎麽辦,顯然簡單的記憶體責任鏈不行了,咱們得上升到分布式責任鏈模式的方式,那怎麽實作分布式責任鏈呢,咱們可以借助MQ來實作訊息觸發,於是觀察者模式上線,這次咱們借助觀察者模式的思想徹底完成分布式重構。

    ps:果然需求演進的最後就是重構,不重構沒有KPI。

    咱們首先定義一個事件,這個就是訂單流事件:

    @Data
    public classOrderFlowEventimplementsSerializable{
    private String orderNo;
    private String currentFlow;
    private String nextFlow;
    }


    這個事件可以在訂單流發起的時候丟到訊息佇列裏面,然後就可以進行訂單流的流轉了,下面我們來看訊息處理邏輯,咱們使用樣版方法再次進行一次程式碼最佳化,這裏還是一個抽象類,然後我們的,支付、權益、積分只需要實作這個抽象類實作handleEvent邏輯就可以了,此外我們只用一個Topic,當前環節處理完成之後如果還有後續流程則再次發送訊息到訊息佇列,進行下一步處理,此外handlerMap 代表責任鏈名稱和責任鏈處理器的對應關系,handlerChain則是責任鏈的環節配置。

    @Data
    publicabstract classBizHandler{
    String topicName = "biz_handle_topic";
    Map<String, BizHandler> handlerMap = new HashMap<>();
    Map<String, String> handlerChain = new LinkedHashMap<>();
    /**
    * 樣版方法:在收到訂單流的訊息之後將進到這裏進行業務邏輯處理
    *
    @param msg 訂單流訊息
    */

    publicvoidhandle(String msg){
    if (CollectionUtils.isEmpty(handlerMap) || CollectionUtils.isEmpty(handlerChain)) {
    //log.warn("handlerMap or handlerChain is empty");
    return;
    }
    OrderFlowEvent orderFlowEvent = JSON.parseObject(msg, OrderFlowEvent. class);
    String currentFlow = orderFlowEvent.getCurrentFlow();
    String nextFlow = handlerChain.get(currentFlow);
    // 當前環節的處理器進行業務處理
    Result result = handlerMap.get(currentFlow).handleEvent(orderFlowEvent);
    if (!result.isSuccess()) {
    thrownew RuntimeException("handleException");
    }
    if (nextFlow == null) {
    return;
    }
    if (result.isSuccess()) {
    // 處理成功並且還有後續流程則再次向訂單流Topic中發送訊息
    sendFlowMsg(result.getData(), nextFlow, handlerChain.get(nextFlow));
    }
    }
    publicabstract Result handleEvent(OrderFlowEvent orderFlowEvent);
    publicvoidsendFlowMsg(Object data, String currentFlow, String nextFlow){
    OrderFlowEvent orderFlowEvent = new OrderFlowEvent();
    orderFlowEvent.setCurrentFlow(currentFlow);
    orderFlowEvent.setNextFlow(nextFlow);
    MqUtils.sendMsg(topicName, JSON.toJSONString(orderFlowEvent));
    }

    }





    例如倉儲環節可以這樣實作:

    public classStorageBizHandlerextendsBizHandler{
    @Override
    public Result handleEvent(OrderFlowEvent orderFlowEvent){
    System.out.println("StorageBizHandler handle orderFlowEvent=" + JSON.toJSONString(orderFlowEvent));
    return Result.success();
    }
    }

    使用的時候則可以這樣:

    public classOrderFlowClient{
    voidhandleOrder(){
    // 定義一下流程名稱和流程例項的對應關系
    Map<String, BizHandler> handlerMap = new HashMap<>();
    handlerMap.put("Storage"new StorageBizHandler());
    handlerMap.put("PointDeduction"new PointDeductionBizHandler());
    handlerMap.put("Payment"new PaymentBizHandler());
    //註意這裏用LinkedHashMap 保證順序 key標識當前流程 value標識下一個流程
    Map<String, String> handlerChain = new LinkedHashMap<>();
    handlerChain.put("Storage""PointDeduction");
    handlerChain.put("PointDeduction""Payment");
    handlerChain.put("Payment"null);
    // 開啟分布式訂單流轉
    Map.Entry<String, String> first = handlerChain.entrySet().iterator().next();
    String key = first.getKey();
    OrderFlowEvent orderFlowEvent = new OrderFlowEvent();
    orderFlowEvent.setCurrentFlow("Storage");
    orderFlowEvent.setOrderNo("order001123124123");
    handlerMap.get(key).handle(JSON.toJSONString(orderFlowEvent));
    }
    }


    最後咱們完成了這次大的技術演進需求,但是就到此結束了嗎?按照這種設計思路改動之後你發現分布式環境下各種並行問題又出現了,於是你還需要分布式鎖來控制,有了分布式鎖你發現環節失敗了還得引入重試邏輯,重試應該怎麽設計,所以發現到了分布式系統下問題變得復雜了,還得繼續想辦法一個個攻克。

    6、總結

    本文透過一次簡單的需求演進分別講述了責任鏈、樣版方法、策略模式、工廠模式、代理模式、觀察者模式的使用,透過實際場景介紹下不同需求下如何透過適合的設計模式來解決問題。