大多數時候我都是寫一些業務代碼,可能一堆CRUD就能解決問題,可是這樣的工做對技術人的提高並很少,如何讓本身從業務中解脫出來找到寫代碼的樂趣呢,我作過一些嘗試,使用設計模式改善本身的業務代碼就是其中的一種。前端
定義java
請求在一個鏈條上處理,鏈條上的受理者處理完畢以後決定是繼續日後傳遞仍是中斷當前處理流程。web
適用場景算法
適用於多節點的流程處理,每一個節點完成各自負責的部分,節點之間不知道彼此的存在,好比OA的審批流,java web開發中的Filter機制。舉一個生活中的例子,筆者以前租房的時候遇到了所謂的黑中介,租的時候感受本身是上帝,可是壞了東西找他修的時候就像個孫子同樣,中介讓我找門店客服,門店客服又讓我找房東,房東又讓我找她家老公,最終好說歹說才把這事了了(租房必定要找正規中介)。json
實踐經驗後端
筆者目前所作的業務是校園團餐的聚合支付,業務流程很簡單,1.學生打開手機付款碼支付,2.食堂大媽使用機具掃付款碼收款。大學食堂有個背景是這樣的,食堂有補貼,菜品比較便宜,因此學校是不肯意讓社會人士去學校食堂消費的,鑑於此,咱們在支付以前加了一套是否容許支付的檢驗邏輯,大致以下: 設計模式
某檔口只容許某類用戶用戶消費,好比教師檔口只容許教師消費,學生檔口不容許校外用戶消費;app
某個檔口一天只容許某類用戶消費幾回,好比教師食堂一天只容許學生消費一次;ide
針對這幾類狀況我創建了三類過濾器,分別是:工具
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.放油,3.炒菜,4.出鍋,1,2,4都差很少,可是第3步是不同的,炒土豆絲得拿鏟子翻炒,可是炒麻婆豆腐得拿勺子輕推,不然豆腐會爛(疫情宅在家,學了很多菜)。
使用場景
不一樣場景的處理流程,部分邏輯是通用的,能夠放到父類中做爲通用實現,部分邏輯是個性化的,須要子類去個性實現。
實踐經驗
仍是接着以前語音播報的例子來講,後期咱們新加了兩個需求:1.消息推送須要增長trace2.有些通道推送失敗須要重試因此如今的流程變成了這樣:1.trace開始2.通道開始推送3.是否容許重試,若是容許執行重試邏輯4.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的自動織入和上下文的自動傳遞,若是有興趣的能夠看我另外一篇博客jaeger使用初探,爲了支持線程間的上下文傳遞,我增長了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這一層作了收斂。