大多數(shù)時候我都是寫一些業(yè)務代碼,可能一堆 CRUD 就能解決問題,但是這樣的工作對技術人的提升并不多,如何讓自己從業(yè)務中解脫出來找到寫代碼的樂趣呢,我做過一些嘗試,使用設計模式改善自己的業(yè)務代碼就是其中的一種。

“你這代碼寫的像坨屎”,今天我的代碼又被當作典型被 CTO 罵了......于是他給我的建議如下:
責任鏈設計模式
模式定義
請求在一個鏈條上處理,鏈條上的受理者處理完畢之后決定是繼續(xù)往后傳遞還是中斷當前處理流程。
適用場景
適用于多節(jié)點的流程處理,每個節(jié)點完成各自負責的部分,節(jié)點之間不知道彼此的存在,比如 OA 的審批流,Java Web 開發(fā)中的 Filter 機制。
舉一個生活中的例子,筆者之前租房的時候遇到了所謂的黑中介,租的時候感覺自己是上帝,但是壞了東西找他修的時候就像個孫子一樣。
中介讓我找門店客服,門店客服又讓我找房東,房東又讓我找她家老公,最終好說歹說才把這事了了(租房一定要找正規(guī)中介)。
實踐經(jīng)驗
筆者目前所做的業(yè)務是校園團餐的聚合支付,業(yè)務流程很簡單:
學生打開手機付款碼支付。
食堂大媽使用機具掃付款碼收款。
大學食堂有個背景是這樣的,食堂有補貼,菜品比較便宜,所以學校是不愿意讓社會人士去學校食堂消費的,鑒于此,我們在支付之前加了一套是否允許支付的檢驗邏輯。
大體如下:
某檔口只允許某類用戶用戶消費,比如教師檔口只允許教師消費,學生檔口不允許校外用戶消費。
某個檔口一天只允許某類用戶消費幾次,比如教師食堂一天只允許學生消費一次。
-
是否允許非清真學生消費,比如某些清真餐廳,是不允許非清真學生消費的。
針對這幾類情況我建立了三類過濾器,分別是:
SpecificCardUserConsumeLimitFilter:按用戶類型判斷是否允許消費。
DayConsumeTimesConsumeLimitFilter:按日消費次數(shù)判斷是否允許消費。
MuslimConsumeLimitFilter:非清真用戶是否允許消費。
判斷邏輯是先通過 SpecificCardUserConsumeLimitFilter 判斷當前用戶是否可以在此檔口消費。
如果允許繼續(xù)由 DayConsumeTimesConsumeLimitFilter 判斷當天消費次數(shù)是否已用完;如果未用完繼續(xù)由 MuslimConsumeLimitFilter 判斷當前用戶是否滿足清真餐廳的就餐條件,前面三條判斷,只要有一個不滿足就提前返回。
public?boolean?canConsume(String?uid,String?shopId,String?supplierId){
????//獲取用戶信息,用戶信息包含類型(student:學生,teacher:老師,unknown:未知用戶)、名族(han:漢族,mg:蒙古族)
????UserInfo?userInfo?=?getUserInfo(uid);
????//獲取消費限制信息,限制信息包含是否允許非清真消費、每種類型的用戶是否允許消費以及允許消費的次數(shù)
???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?){
????????//迭代調(diào)用過濾器
????????if(index????????????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;
????????????}
????????}
????????????????//其余情況,繼續(xù)往后傳遞
????????????return?filterChain.doFilter(filterChain,?memberInfo,?consumeConfig);
????????}
????//DayConsumeTimesConsumeLimitFilter.doFilter方法
?????public?boolean?doFilter(ConsumeLimitFilterChain?filterChain,UserInfo?userInfo,
?????????????ConsumeConfigInfo?consumeConfigInfo?){
????????????????//獲取某一類型的消費限制,比如student可以消費2次
????????CardConsumeConfig?cardConsumeConfig?=?findSuitCardConfig(userInfo,?consumeConfigInfo);
????????????????//獲取當前用戶今天的消費次數(shù)
????????????????int?consumeCnt?=?getConsumeCnt(userInfo)????????
????????if(consumeCnt?>=?cardConsumeConfig.getDayConsumeTimesLimit()){
????????????????????return?false;
????????????????}
????????????????//其余情況,繼續(xù)往后傳遞
????????????????return?filterChain.doFilter(filterChain,?memberInfo,?consumeConfig);
????????}
總結:將每種限制條件的判斷邏輯封裝到了具體的 Filter 中,如果某種限制條件的邏輯有修改不會影響其他條件,如果需要新加限制條件只需要重新構造一個 Filter 織入到 FilterChain 上即可。
策略設計模式
模式定義
定義一系列的算法,把每一個算法封裝起來,并且使它們可相互替換。
適用場景
主要是為了消除大量的 if else 代碼,將每種判斷背后的算法邏輯提取到具體的策略對象中,當算法邏輯修改時對使用者無感知,只需要修改策略對象內(nèi)部邏輯即可。
這類策略對象一般都實現(xiàn)了某個共同的接口,可以達到互換的目的。
實踐經(jīng)驗
筆者之前有個需求是用戶掃碼支付以后向檔口的收銀設備推送一條支付消息,收銀設備收到消息以后會進行語音播報,邏輯很簡單,就是調(diào)用推送平臺推送一條消息給設備即可。
但是由于歷史原因,某些設備對接的推送平臺是不一樣的,A 類設備優(yōu)先使用信鴿推送,如果失敗了需要降級到長輪詢機制,B 類設備直接使用自研的推送平臺即可。
還有個現(xiàn)狀是 A 類和 B 類的消息格式是不一樣的(不同的團隊開發(fā),后期被整合到一起)。
鑒于此,我抽象出 PushStrategy 接口,其具體的實現(xiàn)有 IotPushStrategy 和 XingePushStrategy,分別對應自研推送平臺的推送策略和信鴿平臺的推送策略,使用者時針對不同的設備類型使用不同的推送策略即可。
/**
?*?推送策略
?*?/
public?interface?PushStrategy?{
????/**
?????????@param?deviceVO設備對象,包扣設備sn,信鴿pushid
?????????@param?content,推送內(nèi)容,一般為json
????????*/
????public?CallResult?push(AppDeviceVO?deviceVO,?Object?content);
}
IotPushStrategy?implements?PushStrategy{
????????/**
?????????@param?deviceVO設備對象,包扣設備sn,信鴿pushid
?????????@param?content,推送內(nèi)容,一般為json
????????*/
????public?CallResult?push(AppDeviceVO?deviceVO,?Object?content){
????????????//創(chuàng)建自研推送平臺需要的推送報文
????????????Message?message?=?createPushMsg(deviceVO,content);
????????????//調(diào)用推送平臺推送接口
????????????IotMessageService.pushMsg(message);
????????}
}
XingePushStrategy?implements?PushStrategy{
????????/**
?????????@param?deviceVO設備對象,包扣設備sn,信鴿pushid
?????????@param?content,推送內(nèi)容,一般為json
????????*/
????public?CallResult?push(AppDeviceVO?deviceVO,?Object?content){
????????????//創(chuàng)建信鴿平臺需要的推送報文
????????????JSONObject?jsonObject?=?createPushMsg(content);
????????????//調(diào)用推送平臺推送接口
????????????if(!XinggePush.pushMsg(message)){
????????????????//降級到長輪詢
????????????????...
????????????}
????????}
}
/**
消息推送Service
*/
MessagePushService{
????pushMsg(AppDeviceVO?deviceVO,?Object?content){
????????if(A設備){
????????????XingePushStrategy.push(deviceVO,content);
????????}?else?if(B設備){
????????????IotPushStrategy.push(deviceVO,content);
????????}
????}
}
總結:將每種通道的推送邏輯封裝到了具體的策略中,某種策略的變更不會影響其他策略,由于實現(xiàn)了共同接口,所以策略可以互相替換,對使用者友好。
比如 Java ThreadPoolExecutor 中的任務拒絕策略,當線程池已經(jīng)飽和的時候會執(zhí)行拒絕策略,具體的拒絕邏輯被封裝到了 RejectedExecutionHandler 的 rejectedExecution 中。
模板設計模式
模式定義
模板的價值就在于骨架的定義,骨架內(nèi)部將問題處理的流程已經(jīng)定義好,通用的處理邏輯一般由父類實現(xiàn),個性化的處理邏輯由子類實現(xiàn)。
比如炒土豆絲和炒麻婆豆腐,大體邏輯都是:
切菜
放油
炒菜
出鍋
1,2,4 都差不多,但是第 3 步是不一樣的,炒土豆絲得拿鏟子翻炒,但是炒麻婆豆腐得拿勺子輕推,否則豆腐會爛(疫情宅在家,學了不少菜)。
使用場景
不同場景的處理流程,部分邏輯是通用的,可以放到父類中作為通用實現(xiàn),部分邏輯是個性化的,需要子類去個性實現(xiàn)。
實踐經(jīng)驗
還是接著之前語音播報的例子來說,后期我們新加了兩個需求:
消息推送需要增加 trace。
有些通道推送失敗需要重試。
所以現(xiàn)在的流程變成了這樣:
trace 開始。
通道開始推送。
是否允許重試,如果允許執(zhí)行重試邏輯。
trace 結束。
其中 1 和 4 是通用的,2 和 3 是個性化的,鑒于此我在具體的推送策略之前增加了一層父類的策略,將通用邏輯放到了父類中。
abstract?class?AbstractPushStrategy?implements?PushStrategy{
????@Override
????public?CallResult?push(AppDeviceVO?deviceVO,?Object?content)?{
????????//1.構造span
????????Span?span?=?buildSpan();
????????//2.具體通道推送邏輯由子類實現(xiàn)
????????CallResult?callResult?=?doPush(deviceVO,?content);
????????//3.是否允許重試邏輯由子類實現(xiàn),如果允許執(zhí)行重試邏輯
????????if(!callResult.isSuccess()?&&?canRetry()){
????????????doPush(deviceVO,?content);
????????}
????????//4.trace結束
????????span.finish()?
????}
????//具體推送邏輯由子類實現(xiàn)
????protected?abstract?CallResult?doPush(AppDeviceVO?deviceDO,?Object?content)?;
????//是否允許重試由子類實現(xiàn),有些通道之前沒有做消息排重,所有不能重試
????protected?abstract?boolean?canRetry(CallResult?callResult);
}
XingePushStrategy?extends?AbstractPushStrategy{
????@Override
????protected?CallResult?doPush(AppDeviceVO?deviceDO,?Object?content)?{
????????//執(zhí)行推送邏輯
????}
????@Override
????protected?boolean?canRetry(CallResult?callResult){
????????return?false
????}
}
總結:通過模板定義了流程,將通用邏輯放在父類實現(xiàn),減少了重復代碼,個性化邏輯由子類自己實現(xiàn),子類間修改代碼互不干擾也不會破壞流程。
觀察者設計模式
模式定義
顧名思義,此模式需要有觀察者(Observer)和被觀察者(Observable)兩類角色。
當 Observable 狀態(tài)變化時會通知 Observer,Observer 一般會實現(xiàn)一類通用的接口。
比如 java.util.Observer,Observable 需要通知 Observer 時,逐個調(diào)用 Observer 的 update 方法即可,Observer 的處理成功與否不應該影響 Observable 的流程。
使用場景
一個對象(Observable)狀態(tài)改變需要通知其他對象,Observer 的存在不影響 Observable 的處理結果,Observer 的增刪對 Observable 無感知。
比如 Kafka 的消息訂閱,Producer 發(fā)送一條消息到 Topic,至于是 1 個還是 10 個 Consumer 訂閱這個 Topic,Producer 是不需要關注的。
實踐經(jīng)驗
在責任鏈設計模式那塊我通過三個 Filter 解決了消費限制檢驗的問題,其中有一個 Filter 是用來檢驗消費次數(shù)的,我這里只是讀取用戶的消費次數(shù),那么消費次數(shù)的累加是怎么完成的呢?
其實累加這塊就用到了觀察者模式,具體來講是這樣,當交易系統(tǒng)收到支付成功回調(diào)時會通過 Spring 的事件機制發(fā)布“支付成功事件”。
這樣負責累加消費次數(shù)和負責語音播報的訂閱者就會收到“支付成功事件”,進而做各自的業(yè)務邏輯。

/**
支付回調(diào)處理者
*/
PayCallBackController?implements?ApplicationContextAware?{
?????private?ApplicationContext?applicationContext;
????//如果想獲取applicationContext需要實現(xiàn)ApplicationContextAware接口,Spring容器會回調(diào)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發(fā)布事件,從而達到通知觀察者的目的
????????????this.applicationContext.publishEvent(event);
????????}?
????}
}
/**
?*?語音播報處理者
?*
?*/
public?class?VoiceBroadcastHandler?implements?ApplicationListener<PaySuccessEvent>{
????@Override
????public?void?onApplicationEvent(PaySuccessEvent?event)?{
????????//語音播報邏輯
????}
}
//其他處理者的邏輯類似
總結:觀察者模式將被觀察者和觀察者之間做了解耦,觀察者存在與否不會影響被觀察者的現(xiàn)有邏輯。
裝飾器設計模式
模式定義
裝飾器用來包裝原有的類,在對使用者透明的情況下做功能的增強,比如 Java 中的 BufferedInputStream 可以對其包裝的 InputStream 做增強,從而提供緩沖功能。
使用場景
希望對原有類的功能做增強,但又不希望增加過多子類時,可以使用裝飾器模式來達到同樣的效果。
實踐經(jīng)驗
筆者之前在推動整個公司接入 trace 體系,因此也提供了一些工具來解決 trace 的自動織入和上下文的自動傳遞。
為了支持線程間的上下文傳遞,我增加了 TraceRunnableWrapper 這個裝飾類,從而起到將父線程的上下文透傳到子線程中,對使用者完全透明。
/**
可以自動攜帶trace上下文的Runnable裝飾器
*/
public?class?TraceRunnableWrapper?implements?Runnable{
????//被包裝的目標對象
????private?Runnable?task;
????private?Span?parentSpan?=?null;
????public?TraceRunnableWrapper(Runnable?task)?{
????????//1.獲取當前線程的上下文(因為new的時候還沒有發(fā)生線程切換,所以需要在這里將上下文獲?。?/span>
????????//對這塊代碼感興趣的可以查看opentracing?API
????????io.opentracing.Scope?currentScope?=?GlobalTracer.get().scopeManager().active();
????????//2.保存父上下文
????????parentSpan?=?currentScope.span();
????????this.task?=?task;
????}
????@Override
????public?void?run()?{
????????//run的時候?qū)⒏妇€程的上下文綁定到當前線程
????????io.opentracing.Scope?scope?=?GlobalTracer.get().scopeManager().activate(parentSpan,false);
????????task.run();
????}
}
//使用者
new?Thread(new?Runnable(){run(...)}).start()替換為new?TraceRunnableWrapper(new?Runnable(){run(...)}).start()
總結:使用裝飾器模式做了功能的增強,對使用者來說只需要做簡單的組合就能繼續(xù)使用原功能。
外觀設計模式
模式定義
何為外觀,就是對外提供一個統(tǒng)一的入口:
一是可以影藏系統(tǒng)內(nèi)部的細節(jié)。
二是可以降低使用者的復雜度。
比如 SpringMVC 中的 DispaterServlet,所有的 Controller 都是通過 DispaterServlet 統(tǒng)一暴露。
使用場景
降低使用者的復雜度,簡化客戶端的接入成本。
實踐經(jīng)驗
筆者所在的公司對外提供了一些開放能力給第三方 ISV,比如設備管控、統(tǒng)一支付、對賬單下載等能力。
由于分屬于不同的團隊,所以對外提供的接口形式各異,初期還好,接口不多,ISV 也能接受,但是后期接口多了 ISV 就開始抱怨接入成本太高。
為了解決這一問題,我們在開放接口前面加了一層前端控制器 GatewayController,其實就是我們后來開放平臺的雛形。
GatewayController 對外統(tǒng)一暴露一個接口 gateway.do,將對外接口的請求參數(shù)和響應參數(shù)統(tǒng)一在 GatewayController 做收斂,GatewayController 往后端服務路由時也采用統(tǒng)一接口。

使用者:
HttpClient.doPost("/gateway.do","{'method':'trade.create','sign':'wxxaaa','timestamp':'15311111111'},'bizContent':'業(yè)務參數(shù)'")
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()};
???}
}
總結:采用外觀模式屏蔽了系統(tǒng)內(nèi)部的一些細節(jié),降低了使用者的接入成本。
就拿 GatewayController 來說,ISV 的鑒權,接口的驗簽等重復工作統(tǒng)一由它實現(xiàn)。
ISV 對接不同的接口只需要關心一套接口協(xié)議接口,由 GatewayController 這一層做了收斂。
作者:踩刀詩人
出處:https://urlify.cn/J3mAna
特別推薦一個分享架構+算法的優(yōu)質(zhì)內(nèi)容,還沒關注的小伙伴,可以長按關注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!