從設計模式談業務開發

一、背景

前臺業務同學在業務承接過程中總是抱怨大部分業務無法通過設計模式來承接,寫的代碼是越來越沒有追求,理由是我無法預測未來的業務的發展,且設計模式更多的是在框架或中間件中使用。然而設計模式是對能力抽象出的通用模式,從哲學的角度來看世間萬物皆塵土,事物都是可以抽象出共同的本質的東西。所以,難道只有底層能力可以抽象,業務邏輯部分就不可以抽象了?必須可以纔是啊。在前臺業務承接過程中除了能力可以抽象,還有可以抽象出業務流程,假設在有這樣一些業務場景,品搜和圖搜、直播間評論和點贊、公域直播會場和私域商詳透直播等等,這些各領域內的業務流程“大同小異”,因此都可以抽象出通用的業務流程節點。但是通常在一個主幹流程需要承接的場景有很多,比如直播間互動這個主幹流程包括了直播間評論、點贊、求講解、看證書、進場等等場景,所以我們需要通過主要流程進行進行多場景承接。但是這樣還不夠,在面對多端型多場景的情況下需要處理返回不同的數據模型。綜上所述,我們如何通過一個主幹業務流程承接多個業務場景並在數據上可適配到多端型多場景,實現在服務端高質量高效率的“包接口”,下面會詳細介紹。

二、業務承接

如果你面臨的問題是在同一個業務域上承接多種類似的業務場景,每天在適配各種端型或者各種場景而對外提供接口時,爲了保證應用系統的可擴展性和高效承接業務,那麼可以按照如下步驟進行設計。

2.1 業務流程抽象

首先需要進行業務建模,抽象出用戶用例或者user story,當然具體的粒度可以自己把控,具體如下:

2.1.1 用戶用例

在直播間互動領域內的用戶用例如下:

從整個系統出發,挖掘出面向不同的用戶提供的能力有哪些,在這些用例背後需要進行的流程和節點又是什麼。通過這些流程和節點才能進行後續的系統時序和流程抽象,舉例如下

在互動領域內的流程和節點如下:

  • 風控檢查
  • 評論持久化
  • 評論上屏
  • 評論進溝通

2.2.2 系統時序

基於用戶用例進行分析,這些用例都需要經過什麼的流程節點進行處理,然後將這些流程按照系統時序進行呈現。

到此基於上述的用例和時序是不是可以抽象出具體互動流程了,顯而易見。

2.2.3 業務流程抽象

有了系統用例和系統時序這一步就比較簡單,從系統時序裏很容易可以抽象出具體的流程和流程中的處理節點,具體如下:

  • 對於直播間互動領域可以抽象出的業務流程:風控檢查->互動內容持久化-》消息上屏-》互動進IM
  • 對於直播間分發領域可以抽象出的業務流程:直播推薦-》直播間基礎信息-》直播流信息-》直播間品信息

到此,大家可以按照上述步驟在大腦裏對自己的業務域進行抽象出來了。

2.2.4 設計模式固化主流程

按照業務主流程可以通過模板模式將處理流程固定下來,如下所示:

@Override
@LiveLog(logResult = true)
public InteractionResult interactionSubmit(MobileInteractionRequest request, InteractionLiveRoom liveRoom) {
    Boolean needSave = MapUtils.getBoolean(request.getExtInfo(), LiveInteractionConstant.NEED_SAVE);
    // 默認保存
    InteractionResult saveResult = null;
    if (Objects.isNull(request.getExtInfo()) || Objects.isNull(needSave) || needSave) {
        saveResult = save(request, liveRoom);
        if(Objects.nonNull(saveResult) && !saveResult.isSuccess()) {
            return saveResult;
        }
    }
    // 默認進溝通
    InteractionResult chatResult;
    if (Objects.isNull(request.getSendToChat()) || Boolean.parseBoolean(request.getSendToChat())) {
        chatResult = sendToChat(request);
        if(Objects.nonNull(chatResult) && !chatResult.isSuccess()) {
            return chatResult;
        }
    }
    if(Objects.nonNull(saveResult) && saveResult.isSuccess()) {
        return saveResult;
    }
    return null;
}
/**
 * 互動行爲保存到數據庫或者緩存中
 *
 * @param request
 * @return
 */
protected abstract InteractionResult save(MobileInteractionRequest request, InteractionLiveRoom liveRoom);
/**
 * 進溝通
 *
 * @param request
 * @return
 */
protected abstract InteractionResult sendToChat(MobileInteractionRequest request);

2.2 業務流程擴展

因在上述模版模式中預留了兩個擴展點,所以在子類中可以通過擴展點進行擴展,舉例如下:

如果有更多的場景就需要擴展實現上述兩個擴展點進行擴展即可,這樣保證了業務的高效承接。這裏會有兩個問題:

  • 在程序運行時如何根據具體的場景選擇哪個子類進行邏輯處理
  • 如何進行適配端和場景返回的數據模型

針對第一個問題,其實就是如何去if else的問題,這裏也給出比較經典的方案:

  • 枚舉法
  • 表驅動法
  • 策略模式+工廠模式

其中枚舉法和表驅動法比較簡單易用,原理就是將映射關係封裝在枚舉類或本地緩存中,這裏簡單介紹下如何通過策略模式消除if else。

// 策略接口
public interface Opt {
    int apply(int a, int b);
}
// 策略實現類
@Component(value = "addOpt")
public class AddOpt implements Opt {
    @Autowired
    xxxAddResource resource; // 這裏通過Spring框架注入了資源
    @Override
    public int apply(int a, int b) {
       return resource.process(a, b);
    }
}
// 策略實現類
@Component(value = "devideOpt")
public class devideOpt implements Opt {
    @Autowired
    xxxDivResource resource; // 這裏通過Spring框架注入了資源
    @Override
    public int apply(int a, int b) {
       return resource.process(a, b);
    }
}
// 策略處理
@Component
public class OptStrategyContext{
    private Map<String, Opt> strategyMap = new ConcurrentHashMap<>();
    @Autowired
    public OptStrategyContext(Map<String, TalkService> strategyMap) {
        this.strategyMap.clear();
        this.strategyMap.putAll(strategyMap);
    }
    public int apply(Sting opt, int a, int b) {
        return strategyMap.get(opt).apply(a, b);
    }
}

總結僞代碼:

// 抽象類固定業務流程 預留擴展點
public abstract class AbstractXxxx {
  doXxx(Object context) {
      // 節點1
        doNode1(context); 
        // 節點2

        doNode2(context); 
        // 節點3
        doNode3(context); 
        // 節點n
        ...
    }
    // 擴展點1
    protected abstract Result doNode1(Object context);
    // 擴展點2
    protected abstract Result doNode2(Object context);
    // 擴展點3

    protected abstract Result doNode3(Object context);
}    
// 策略處理
public class OptStrategyContext{
    private Map<String, Opt> strategyMap = new ConcurrentHashMap<>();
    static {
        // 上述模版模式的實現類
      strategyMap.put("business1", Xxxx1);
        strategyMap.put("business2", Xxxx2);
        strategyMap.put("business3", Xxxx3);
    }
    // 初始化
    public OptStrategyContext(Map<String, Opt> strategyMap) {
        this.strategyMap.clear();
        this.strategyMap.putAll(strategyMap);
    }
    public int doXxxx(Object context) {
        return strategyMap.get(business).doXxxx(context);
    }
}

2.3 多場景多端型適配

上面我們只是通過模版模式抽象出了主幹業務流程,但是如何適配不同的端型和不同的場景,返回不同的數據模型呢,這裏有兩種答案,一種是模版模式、另一種是“棒棒糖”模式,下面逐一介紹。

2.3.1 模版模式適配

既然是模版模式,這裏的主幹流程又是什麼呢?主要跟我們解決的問題有關係,按照2.1中的流程步驟,可以抽象出固定的流程爲:請求入參處理-》業務邏輯處理-》結果返回處理。

其中業務邏輯處理可以選定爲2.2中介紹的通過策略模式選擇業務擴展的子類,來處裏業務部分;請求入參和結果返回處理部分可以設置爲擴展點,供子類擴展。具體僞代碼如下:

// 抽象類固定業務流程 預留擴展點 適配多端型多場景
public abstract class AbstractSceneAdapter {
  <T> T doXxx(Object context) {
      // 節點1
        doRequestFilter(context); 
        // 節點2
        getBusinessService(context).doBusiness(context);  
        // 節點3
        return doResultWrap(context); 
    }
    // 擴展點1
    protected abstract Result doRequestFilter(Object context);
    // 擴展點2
    protected abstract Result doBusiness(Object context);
    // 擴展點3
    protected abstract Result doResultWrap(Object context);
    // 業務邏輯處理子類
    protected abstract BusinessService getBusinessService(Object context);
}    
// 策略處理 根據不同端型場景選擇合適的子類
public class SceneAdapterViewService {
    private Map<String, SceneAdapter> strategyMap = new ConcurrentHashMap<>();
    static {
        // 上述模版模式的實現類
      strategyMap.put("scene1", Xxxx1);
        strategyMap.put("scene2", Xxxx2);
        strategyMap.put("scene3", Xxxx3);
    }
    // 初始化
    public SceneAdapterViewService(Map<String, SceneAdapter> strategyMap) {
        this.strategyMap.clear();
        this.strategyMap.putAll(strategyMap);
    }
    public Result doXxxx(Object context) {
        return strategyMap.get(scene).doXxxx(context);
    }
}

注:因要適配不同端型不同場景返回不同的數據模型,所以上述僞代碼中主流程最終返回的結果是一個泛型,在子類實現的時候進行確定具體返回的類型。

2.3.1 棒棒糖模式適配

通過模版模式來適配時會有一個小問題,當需要有多個請求入參處理器或者多個結果包裝器的時候需要在模版裏增加處理節點,但其實這些節點是有共性的可抽象出來的。因此可以針對入參處理器和結果包裝器定義單獨的接口,需要多個處理器時同時實現接口進行處理。然後這些實現類打包放在單獨的類中依次執行即可。當然其中的業務處理部分也可以定義接口動態實現。僞代碼如下:

// 入參處理器
public interface IRequestFilter<> {
  void doFilter(T t);
}
// 結果包裝器
public interface IResultWrapper<R, T> {
  Result<R> doWrap(Result<T> res);
}
public class SceneAdapterViewService implements InitializingBean {
  private List<IRequestFilter> filters;
    private List<IResultWrapper> wrappers;
    private Map<String, SceneAdapter> strategyMap = new ConcurrentHashMap<>();
    // 請求過濾器實現類
  @Autowired
    @Qualifier("filter1")
    private IRequestFilter filter1;
    @Autowired
    @Qualifier("filter2")
    private IRequestFilter filter2;
    // 結果處理器實現類
    @Autowired
    @Qualifier("wrapper1")
    private IResultWrapper wrapper1;
    // 業務處理實現類
    @Autowired
    @Qualifier("scene1")
    private SceneAdapter scene1;
    @Autowired
    @Qualifier("scene2")
    private SceneAdapter scene2;
    @Autowired
    @Qualifier("scene3")
    private SceneAdapter scene3;
  // 主方法
    publice Result sceneAdapte(Object context) {
        // 請求入參過濾  異常時返回
        for(int i = 0; i<filters.size(); i++) {
            try {
                filters.get(i).doFilter(context)
            } catch {
                return null;
            }
        }
        // 策略模式執行業務邏輯,執行是按照模版模式
      Result res = strategyMap.get(scene).doXxxx(context);
        Result result = res;
        // 過濾處理,包括樹結構改變,數據字段裁剪等
        for(int i = 0; i<wrappers.size(); i++) {
            try {
                result = wrappers.get(i).doWrap(result)
            } catch {
                return res;
            }
        }
        return result;
    }
    // 初始化各個節點值
    @Override
    public void afterPropertiesSet() throws Exception {
        // 入參過濾器 可多個
        filters.add(filter1);
        filters.add(filter2);
        // 結果處理器 可多個
        wrappers.add(wrapper1);

        // 業務處理部分
        strategyMap.put("scene1", Xxxx1);
        strategyMap.put("scene2", Xxxx2);
        strategyMap.put("scene3", Xxxx3);
    }
}

三、接口設計

基於上述兩種設計模式來適配時我們的接口又該如何設計,是設計面向通用的業務層接口還是面向定製化的業務接口,兩種方式各有優缺點:

  優點 缺點
通用業務接口 1,擴展性強2,業務承接效率高,無需頻繁發佈代碼 1,業務邊界不清晰,問題排查效率低2,接口數據冗餘
定製化業務接口 1,業務邊界清晰2,接口出入參數據無冗餘 1,無擴展2,頻繁包接口發佈代碼


對於接口提供者來說肯定不希望頻繁改動代碼發佈代碼,但是又希望能夠在業務承接過程中能夠高效適配多端型多場景,因此這裏總結了下接口設計原則:

1、對於越底層的接口應該越通用,例如HSF接口、領域服務、中間件提供的接口;

2、對於越上層的接口應該越定製化,例如對於不同的UI適配、不同的場景適配等;

3、對於業務領域內的接口應該通用化,例如直播業務域的分發領域、互動領域內的接口儘可能的通用化;

四、總結

在承接業務過程中會面臨頻繁包接口、一個view層的數據模型充滿了小100個屬性,系統的擴展性遇到瓶頸,這些問題除了通過平臺化配置化的能力來解決,但是迴歸到代碼本身我們任然可以通過抽象的設計模式來解決。

  • 基於抽象的理論達到複用、高內聚低耦合,降低系統複雜度的目標,設計模式不只是用在底層能力或中間件中,在業務承接過程中亦有大的功效。
  • 千萬不要爲了用設計模式而刻意使用設計模式,帶來的效果適得其反,在選擇設計模式時也要三思,落地後再改動成本將會巨大。
  • 在前臺業務開發中,需要劃分主各個業務領域,在領域中抽象出該業務的處理流程,基於流程可設計相關的擴展和編排能力,方式有很多種,包括SPI、設計模式、DSL等,本文主要通過模版模式和棒棒糖模式來解決問題。
  • 接口設計應該按照越底層越通用,越上層越定製化的原則進行設計,當然在業務域內的接口應儘可能的通用話。

作者|幽霄

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章