我寫的代碼,又被CTO罵了......

點擊上方藍色字關注咱們~








大多數時候我都是寫一些業務代碼,可能一堆 CRUD 就能解決問題,可是這樣的工做對技術人的提高並很少,如何讓本身從業務中解脫出來找到寫代碼的樂趣呢,我作過一些嘗試,使用設計模式改善本身的業務代碼就是其中的一種。前端


圖片來自 Pexels
java


「你這代碼寫的像坨屎,今天個人代碼又被看成典型被 CTO 罵了......因而他給個人建議以下:
python


責任鏈設計模式web


模式定義面試


請求在一個鏈條上處理,鏈條上的受理者處理完畢以後決定是繼續日後傳遞仍是中斷當前處理流程。
算法


適用場景json


適用於多節點的流程處理,每一個節點完成各自負責的部分,節點之間不知道彼此的存在,好比 OA 的審批流,Java Web 開發中的 Filter 機制。
小程序


舉一個生活中的例子,筆者以前租房的時候遇到了所謂的黑中介,租的時候感受本身是上帝,可是壞了東西找他修的時候就像個孫子同樣。
後端


中介讓我找門店客服,門店客服又讓我找房東,房東又讓我找她家老公,最終好說歹說才把這事了了(租房必定要找正規中介)。
設計模式


實踐經驗


筆者目前所作的業務是校園團餐的聚合支付,業務流程很簡單:

  • 學生打開手機付款碼支付。

  • 食堂大媽使用機具掃付款碼收款。


大學食堂有個背景是這樣的,食堂有補貼,菜品比較便宜,因此學校是不肯意讓社會人士去學校食堂消費的,鑑於此,咱們在支付以前加了一套是否容許支付的檢驗邏輯。


大致以下:

  • 某檔口只容許某類用戶用戶消費,好比教師檔口只容許教師消費,學生檔口不容許校外用戶消費。

  • 某個檔口一天只容許某類用戶消費幾回,好比教師食堂一天只容許學生消費一次。

  • 是否容許非清真學生消費,好比某些清真餐廳,是不容許非清真學生消費的。


針對這幾類狀況我創建了三類過濾器,分別是:

  • SpecificCardUserConsumeLimitFilter:按用戶類型判斷是否容許消費。

  • DayConsumeTimesConsumeLimitFilter:按日消費次數判斷是否容許消費。

  • MuslimConsumeLimitFilter:非清真用戶是否容許消費。


判斷邏輯是先經過 SpecificCardUserConsumeLimitFilter 判斷當前用戶是否能夠在此檔口消費。


若是容許繼續由 DayConsumeTimesConsumeLimitFilter 判斷當天消費次數是否已用完;若是未用完繼續由 MuslimConsumeLimitFilter 判斷當前用戶是否知足清真餐廳的就餐條件,前面三條判斷,只要有一個不知足就提早返回。


部分代碼以下:
public boolean canConsume(String uid,String shopId,String supplierId){
    //獲取用戶信息,用戶信息包含類型(student:學生,teacher:老師,unknown:未知用戶)、名族(han:漢族,mg:蒙古族)
    UserInfo userInfo = getUserInfo(uid);
    //獲取消費限制信息,限制信息包含是否容許非清真消費、每種類型的用戶是否容許消費以及容許消費的次數
   ConsumeConfigInfo consumeConfigInfo = getConsumeConfigInfo(shopId,supplierId) 


    // 構造消費限制過濾器鏈條
    ConsumeLimitFilterChain filterChain = new ConsumeLimitFilterChain();
    filterChain.addFilter(new SpecificCardUserConsumeLimitFilter());
    filterChain.addFilter(new DayConsumeTimesConsumeLimitFilter());
    filterChain.addFilter(new MuslimConsumeLimitFilter());
    boolean checkResult = filterChain.doFilter(filterChain, schoolMemberInfo, consumeConfigInfo);

    //filterChain.doFilter方法
   public boolean doFilter(ConsumeLimitFilterChain filterChain,UserInfo userInfo,
             ConsumeConfigInfo consumeConfigInfo )
{
        //迭代調用過濾器
        if(index<filters.size()){
            return filters.get(index++).doFilter(filterChain, userInfo, consumeConfigInfo);
        }
    }

    //SpecificCardUserConsumeLimitFilter.doFilter方法
     public boolean doFilter(ConsumeLimitFilterChain filterChain,UserInfo userInfo,
             ConsumeConfigInfo consumeConfigInfo )
{
                //獲取某一類型的消費限制,好比student容許消費,unknown不容許消費
        CardConsumeConfig cardConsumeConfig = findSuitCardConfig(userInfo, consumeConfigInfo);

        // 判斷當前卡用戶是否容許消費
        if (consumeCardConfig != null) {
            if ((!CAN_PAY.equals(cardConsumeConfig .getEnabledPay()))) {
                return false;
            }
        }

                //其他狀況,繼續日後傳遞
            return filterChain.doFilter(filterChain, memberInfo, consumeConfig);
        }

    //DayConsumeTimesConsumeLimitFilter.doFilter方法
     public boolean doFilter(ConsumeLimitFilterChain filterChain,UserInfo userInfo,
             ConsumeConfigInfo consumeConfigInfo )
{
                //獲取某一類型的消費限制,好比student能夠消費2次
        CardConsumeConfig cardConsumeConfig = findSuitCardConfig(userInfo, consumeConfigInfo);

                //獲取當前用戶今天的消費次數
                int consumeCnt = getConsumeCnt(userInfo)        
        if(consumeCnt >= cardConsumeConfig.getDayConsumeTimesLimit()){
                    return false;
                }

                //其他狀況,繼續日後傳遞
                return filterChain.doFilter(filterChain, memberInfo, consumeConfig);
        }

總結:將每種限制條件的判斷邏輯封裝到了具體的 Filter 中,若是某種限制條件的邏輯有修改不會影響其餘條件,若是須要新加限制條件只須要從新構造一個 Filter 織入到 FilterChain 上便可。


策略設計模式


模式定義


定義一系列的算法,把每個算法封裝起來,而且使它們可相互替換。


適用場景


主要是爲了消除大量的 if else 代碼,將每種判斷背後的算法邏輯提取到具體的策略對象中,當算法邏輯修改時對使用者無感知,只須要修改策略對象內部邏輯便可。


這類策略對象通常都實現了某個共同的接口,能夠達到互換的目的。


實踐經驗


筆者以前有個需求是用戶掃碼支付之後向檔口的收銀設備推送一條支付消息,收銀設備收到消息之後會進行語音播報,邏輯很簡單,就是調用推送平臺推送一條消息給設備便可。


可是因爲歷史緣由,某些設備對接的推送平臺是不同的,A 類設備優先使用信鴿推送,若是失敗了須要降級到長輪詢機制,B 類設備直接使用自研的推送平臺便可。


還有個現狀是 A 類和 B 類的消息格式是不同的(不一樣的團隊開發,後期被整合到一塊兒)。


鑑於此,我抽象出 PushStrategy 接口,其具體的實現有 IotPushStrategy 和 XingePushStrategy,分別對應自研推送平臺的推送策略和信鴿平臺的推送策略,使用者時針對不一樣的設備類型使用不一樣的推送策略便可。


部分代碼以下:
/**
 * 推送策略
 * /
public interface PushStrategy {
    /**
         @param deviceVO設備對象,包扣設備sn,信鴿pushid
         @param content,推送內容,通常爲json
        */

    public CallResult push(AppDeviceVO deviceVO, Object content);
}

IotPushStrategy implements PushStrategy{
        /**
         @param deviceVO設備對象,包扣設備sn,信鴿pushid
         @param content,推送內容,通常爲json
        */

    public CallResult push(AppDeviceVO deviceVO, Object content){
            //建立自研推送平臺須要的推送報文
            Message message = createPushMsg(deviceVO,content);

            //調用推送平臺推送接口
            IotMessageService.pushMsg(message);
        }
}

XingePushStrategy implements PushStrategy{
        /**
         @param deviceVO設備對象,包扣設備sn,信鴿pushid
         @param content,推送內容,通常爲json
        */

    public CallResult push(AppDeviceVO deviceVO, Object content){
            //建立信鴿平臺須要的推送報文
            JSONObject jsonObject = createPushMsg(content);

            //調用推送平臺推送接口
            if(!XinggePush.pushMsg(message)){
                //降級到長輪詢
                ...
            }
        }
}

/**
消息推送Service
*/

MessagePushService{
    pushMsg(AppDeviceVO deviceVO, Object content){
        if(A設備){
            XingePushStrategy.push(deviceVO,content);
        } else if(B設備){
            IotPushStrategy.push(deviceVO,content);
        }
    }
}

總結:將每種通道的推送邏輯封裝到了具體的策略中,某種策略的變動不會影響其餘策略,因爲實現了共同接口,因此策略能夠互相替換,對使用者友好。


好比 Java ThreadPoolExecutor 中的任務拒絕策略,當線程池已經飽和的時候會執行拒絕策略,具體的拒絕邏輯被封裝到了 RejectedExecutionHandler 的 rejectedExecution 中。


模板設計模式


模式定義


模板的價值就在於骨架的定義,骨架內部將問題處理的流程已經定義好,通用的處理邏輯通常由父類實現,個性化的處理邏輯由子類實現。


好比炒土豆絲和炒麻婆豆腐,大致邏輯都是:

  • 切菜

  • 放油

  • 炒菜

  • 出鍋


1,2,4 都差很少,可是第 3 步是不同的,炒土豆絲得拿鏟子翻炒,可是炒麻婆豆腐得拿勺子輕推,不然豆腐會爛(疫情宅在家,學了很多菜)。


使用場景


不一樣場景的處理流程,部分邏輯是通用的,能夠放到父類中做爲通用實現,部分邏輯是個性化的,須要子類去個性實現。


實踐經驗


仍是接着以前語音播報的例子來講,後期咱們新加了兩個需求:

  • 消息推送須要增長 trace。

  • 有些通道推送失敗須要重試。


因此如今的流程變成了這樣:

  • trace 開始。

  • 通道開始推送。

  • 是否容許重試,若是容許執行重試邏輯。

  • trace 結束。


其中 1 和 4 是通用的,2 和 3 是個性化的,鑑於此我在具體的推送策略以前增長了一層父類的策略,將通用邏輯放到了父類中。


修改後的代碼以下:
abstract class AbstractPushStrategy implements PushStrategy{
    @Override
    public CallResult push(AppDeviceVO deviceVO, Object content) {
        //1.構造span
        Span span = buildSpan();
        //2.具體通道推送邏輯由子類實現
        CallResult callResult = doPush(deviceVO, content);

        //3.是否容許重試邏輯由子類實現,若是容許執行重試邏輯
        if(!callResult.isSuccess() && canRetry()){
            doPush(deviceVO, content);
        }

        //4.trace結束
        span.finish() 
    }

    //具體推送邏輯由子類實現
    protected abstract CallResult doPush(AppDeviceVO deviceDO, Object content) ;

    //是否容許重試由子類實現,有些通道以前沒有作消息排重,全部不能重試
    protected abstract boolean canRetry(CallResult callResult);

}

XingePushStrategy extends AbstractPushStrategy{
    @Override
    protected CallResult doPush(AppDeviceVO deviceDO, Object content) {
        //執行推送邏輯
    }

    @Override
    protected boolean canRetry(CallResult callResult){
        return false
    }
}

總結:經過模板定義了流程,將通用邏輯放在父類實現,減小了重複代碼,個性化邏輯由子類本身實現,子類間修改代碼互不干擾也不會破壞流程。


觀察者設計模式


模式定義


顧名思義,此模式須要有觀察者(Observer)和被觀察者(Observable)兩類角色。


當 Observable 狀態變化時會通知 Observer,Observer 通常會實現一類通用的接口。


好比 java.util.Observer,Observable 須要通知 Observer 時,逐個調用 Observer 的 update 方法便可,Observer 的處理成功與否不該該影響 Observable 的流程。


使用場景


一個對象(Observable)狀態改變須要通知其餘對象,Observer 的存在不影響 Observable 的處理結果,Observer 的增刪對 Observable 無感知。


好比 Kafka 的消息訂閱,Producer 發送一條消息到 Topic,至因而 1 個仍是 10 個 Consumer 訂閱這個 Topic,Producer 是不須要關注的。


實踐經驗


在責任鏈設計模式那塊我經過三個 Filter 解決了消費限制檢驗的問題,其中有一個 Filter 是用來檢驗消費次數的,我這裏只是讀取用戶的消費次數,那麼消費次數的累加是怎麼完成的呢?


其實累加這塊就用到了觀察者模式,具體來說是這樣,當交易系統收到支付成功回調時會經過 Spring 的事件機制發佈「支付成功事件」。


這樣負責累加消費次數和負責語音播報的訂閱者就會收到「支付成功事件」,進而作各自的業務邏輯。


畫個簡單的圖描述一下:
代碼結構大致以下:
/**
支付回調處理者
*/

PayCallBackController implements ApplicationContextAware {
     private ApplicationContext applicationContext;

    //若是想獲取applicationContext須要實現ApplicationContextAware接口,Spring容器會回調setApplicationContext方法將applicationContext注入進來
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException 
{
        this.applicationContext = applicationContext;
    }
     @RequestMapping(value = "/pay/callback.do")
     public View callback(HttpServletRequest request){
        if(paySuccess(request){
            //構造支付成功事件
            PaySuccessEvent event = buildPaySuccessEvent(...);
            //經過applicationContext發佈事件,從而達到通知觀察者的目的
            this.applicationContext.publishEvent(event);
        } 
    }
}
/**
 * 語音播報處理者
 *
 */

public class VoiceBroadcastHandler implements ApplicationListener<PaySuccessEvent>{
    @Override
    public void onApplicationEvent(PaySuccessEvent event) {
        //語音播報邏輯
    }
}

//其餘處理者的邏輯相似

總結:觀察者模式將被觀察者和觀察者之間作了解耦,觀察者存在與否不會影響被觀察者的現有邏輯。


裝飾器設計模式


模式定義


裝飾器用來包裝原有的類,在對使用者透明的狀況下作功能的加強,好比 Java 中的 BufferedInputStream 能夠對其包裝的 InputStream 作加強,從而提供緩衝功能。


使用場景


但願對原有類的功能作加強,但又不但願增長過多子類時,可使用裝飾器模式來達到一樣的效果。


實踐經驗


筆者以前在推進整個公司接入 trace 體系,所以也提供了一些工具來解決 trace 的自動織入和上下文的自動傳遞。


爲了支持線程間的上下文傳遞,我增長了 TraceRunnableWrapper 這個裝飾類,從而起到將父線程的上下文透傳到子線程中,對使用者徹底透明。


代碼以下:
/**
能夠自動攜帶trace上下文的Runnable裝飾器
*/

public class TraceRunnableWrapper implements Runnable{
    //被包裝的目標對象
    private Runnable task;
    private Span parentSpan = null;

    public TraceRunnableWrapper(Runnable task{
        //1.獲取當前線程的上下文(由於new的時候尚未發生線程切換,因此須要在這裏將上下文獲取)
        //對這塊代碼感興趣的能夠查看opentracing API
        io.opentracing.Scope currentScope = GlobalTracer.get().scopeManager().active();
        //2.保存父上下文
        parentSpan = currentScope.span();
        this.task = task;
    }

    @Override
    public void run() 
{
        //run的時候將父線程的上下文綁定到當前線程
        io.opentracing.Scope scope = GlobalTracer.get().scopeManager().activate(parentSpan,false);
        task.run();
    }
}

//使用者
new Thread(new Runnable(){run(...)}).start()替換爲new TraceRunnableWrapper(new Runnable(){run(...)}).start()

總結:使用裝飾器模式作了功能的加強,對使用者來講只須要作簡單的組合就能繼續使用原功能。


外觀設計模式


模式定義


何爲外觀,就是對外提供一個統一的入口:

  • 一是能夠影藏系統內部的細節。

  • 二是能夠下降使用者的複雜度。


好比 SpringMVC 中的 DispaterServlet,全部的 Controller 都是經過 DispaterServlet 統一暴露。


使用場景


下降使用者的複雜度,簡化客戶端的接入成本。


實踐經驗


筆者所在的公司對外提供了一些開放能力給第三方 ISV,好比設備管控、統一支付、對帳單下載等能力。


因爲分屬於不一樣的團隊,因此對外提供的接口形式各異,初期還好,接口很少,ISV 也能接受,可是後期接口多了 ISV 就開始抱怨接入成本過高。


爲了解決這一問題,咱們在開放接口前面加了一層前端控制器 GatewayController,其實就是咱們後來開放平臺的雛形。


GatewayController 對外統一暴露一個接口 gateway.do,將對外接口的請求參數和響應參數統一在 GatewayController 作收斂,GatewayController 日後端服務路由時也採用統一接口。


改造先後對好比下圖:
大概代碼以下:
使用者:
HttpClient.doPost("/gateway.do","{'method':'trade.create','sign':'wxxaaa','timestamp':'15311111111'},'bizContent':'業務參數'")

GatewayController:
@RequestMapping("/gateway.do")
JSON gateway(HttpServletRequest req){
   //1.組裝開放請求
   OpenRequest openRequest = buildOpenRequest(req);

   OpenResponse openResponse = null;
   //2.請求路由
   if("trade.create".equals(openRequest.getMethod()){
       //proxy to trade service by dubbo
       openResponse = TradeFacade.execute(genericParam);
   } else if("iot.message.push".equals(openRequest.getMethod()){
       //proxy to iot service by httpclient
        openResponse = HttpClient.doPost('http://iot.service/generic/execute'genericParam);
   }

   if(openResponse.isSuccess()){
        return {"code":"10000","bizContent":openResponse.getResult()};
   }else{
        return {"code":"20000","bizCode":openResponse.getCode()};
   }


}

總結:採用外觀模式屏蔽了系統內部的一些細節,下降了使用者的接入成本。


就拿 GatewayController 來講,ISV 的鑑權,接口的驗籤等重複工做統一由它實現。


ISV 對接不一樣的接口只須要關心一套接口協議接口,由 GatewayController 這一層作了收斂。


在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝個人哈



往期熱門文章:

1,架構的本質:如何打造一個有序的系統?
2, 分佈式高可靠之負載均衡,今天看了你確定會
3, 分佈式數據之緩存技術,一塊兒來揭開其神祕面紗
4,分佈式數據複製技術,今天就教你真正分身術
5, 數據分佈方式之哈希與一致性哈希,我就是個神算子
分佈式存儲系統三要素,掌握這些就離成功不遠了
想要設計一個好的分佈式系統,必須搞定這個理論
分佈式通訊技術之發佈訂閱,乾貨滿滿
9, 分佈式通訊技術之遠程調用:RPC
10  秒殺系統每秒上萬次下單請求,咱們該怎麼去設計



本文分享自微信公衆號 - 架構師修煉(jiagouxiulian)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索