我寫的代碼,又被CTO罵了......
大多數(shù)時候我都是寫一些業(yè)務(wù)代碼,可能一堆 CRUD 就能解決問題,但是這樣的工作對技術(shù)人的提升并不多,如何讓自己從業(yè)務(wù)中解脫出來找到寫代碼的樂趣呢,我做過一些嘗試,使用設(shè)計模式改善自己的業(yè)務(wù)代碼就是其中的一種。
圖片來自 Pexels
“你這代碼寫的像坨屎”,今天我的代碼又被當(dāng)作典型被 CTO 罵了......于是他給我的建議如下:
責(zé)任鏈設(shè)計模式
模式定義
請求在一個鏈條上處理,鏈條上的受理者處理完畢之后決定是繼續(xù)往后傳遞還是中斷當(dāng)前處理流程。
適用場景
適用于多節(jié)點的流程處理,每個節(jié)點完成各自負(fù)責(zé)的部分,節(jié)點之間不知道彼此的存在,比如 OA 的審批流,Java Web 開發(fā)中的 Filter 機(jī)制。
舉一個生活中的例子,筆者之前租房的時候遇到了所謂的黑中介,租的時候感覺自己是上帝,但是壞了東西找他修的時候就像個孫子一樣。
中介讓我找門店客服,門店客服又讓我找房東,房東又讓我找她家老公,最終好說歹說才把這事了了(租房一定要找正規(guī)中介)。
實踐經(jīng)驗
筆者目前所做的業(yè)務(wù)是校園團(tuán)餐的聚合支付,業(yè)務(wù)流程很簡單:
- 學(xué)生打開手機(jī)付款碼支付。
- 食堂大媽使用機(jī)具掃付款碼收款。
大學(xué)食堂有個背景是這樣的,食堂有補(bǔ)貼,菜品比較便宜,所以學(xué)校是不愿意讓社會人士去學(xué)校食堂消費的,鑒于此,我們在支付之前加了一套是否允許支付的檢驗邏輯。
大體如下:
- 某檔口只允許某類用戶用戶消費,比如教師檔口只允許教師消費,學(xué)生檔口不允許校外用戶消費。
- 某個檔口一天只允許某類用戶消費幾次,比如教師食堂一天只允許學(xué)生消費一次。
- 是否允許非清真學(xué)生消費,比如某些清真餐廳,是不允許非清真學(xué)生消費的。
針對這幾類情況我建立了三類過濾器,分別是:
- SpecificCardUserConsumeLimitFilter:按用戶類型判斷是否允許消費。
- DayConsumeTimesConsumeLimitFilter:按日消費次數(shù)判斷是否允許消費。
- MuslimConsumeLimitFilter:非清真用戶是否允許消費。
判斷邏輯是先通過 SpecificCardUserConsumeLimitFilter 判斷當(dāng)前用戶是否可以在此檔口消費。
如果允許繼續(xù)由 DayConsumeTimesConsumeLimitFilter 判斷當(dāng)天消費次數(shù)是否已用完;如果未用完繼續(xù)由 MuslimConsumeLimitFilter 判斷當(dāng)前用戶是否滿足清真餐廳的就餐條件,前面三條判斷,只要有一個不滿足就提前返回。
部分代碼如下:
- public boolean canConsume(String uid,String shopId,String supplierId){
- //獲取用戶信息,用戶信息包含類型(student:學(xué)生,teacher:老師,unknown:未知用戶)、名族(han:漢族,mg:蒙古族)
- UserInfo userInfo = getUserInfo(uid);
- //獲取消費限制信息,限制信息包含是否允許非清真消費、每種類型的用戶是否允許消費以及允許消費的次數(shù)
- ConsumeConfigInfo consumeConfigInfo = getConsumeConfigInfo(shopId,supplierId)
- // 構(gòu)造消費限制過濾器鏈條
- 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<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);
- // 判斷當(dāng)前卡用戶是否允許消費
- 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);
- //獲取當(dāng)前用戶今天的消費次數(shù)
- int consumeCnt = getConsumeCnt(userInfo)
- if(consumeCnt >= cardConsumeConfig.getDayConsumeTimesLimit()){
- return false;
- }
- //其余情況,繼續(xù)往后傳遞
- return filterChain.doFilter(filterChain, memberInfo, consumeConfig);
- }
總結(jié):將每種限制條件的判斷邏輯封裝到了具體的 Filter 中,如果某種限制條件的邏輯有修改不會影響其他條件,如果需要新加限制條件只需要重新構(gòu)造一個 Filter 織入到 FilterChain 上即可。
策略設(shè)計模式
模式定義
定義一系列的算法,把每一個算法封裝起來,并且使它們可相互替換。
適用場景
主要是為了消除大量的 if else 代碼,將每種判斷背后的算法邏輯提取到具體的策略對象中,當(dāng)算法邏輯修改時對使用者無感知,只需要修改策略對象內(nèi)部邏輯即可。
這類策略對象一般都實現(xiàn)了某個共同的接口,可以達(dá)到互換的目的。
實踐經(jīng)驗
筆者之前有個需求是用戶掃碼支付以后向檔口的收銀設(shè)備推送一條支付消息,收銀設(shè)備收到消息以后會進(jìn)行語音播報,邏輯很簡單,就是調(diào)用推送平臺推送一條消息給設(shè)備即可。
但是由于歷史原因,某些設(shè)備對接的推送平臺是不一樣的,A 類設(shè)備優(yōu)先使用信鴿推送,如果失敗了需要降級到長輪詢機(jī)制,B 類設(shè)備直接使用自研的推送平臺即可。
還有個現(xiàn)狀是 A 類和 B 類的消息格式是不一樣的(不同的團(tuán)隊開發(fā),后期被整合到一起)。
鑒于此,我抽象出 PushStrategy 接口,其具體的實現(xiàn)有 IotPushStrategy 和 XingePushStrategy,分別對應(yīng)自研推送平臺的推送策略和信鴿平臺的推送策略,使用者時針對不同的設(shè)備類型使用不同的推送策略即可。
部分代碼如下:
- /**
- * 推送策略
- * /
- public interface PushStrategy {
- /**
- @param deviceVO設(shè)備對象,包扣設(shè)備sn,信鴿pushid
- @param content,推送內(nèi)容,一般為json
- */
- public CallResult push(AppDeviceVO deviceVO, Object content);
- }
- IotPushStrategy implements PushStrategy{
- /**
- @param deviceVO設(shè)備對象,包扣設(shè)備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設(shè)備對象,包扣設(shè)備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設(shè)備){
- XingePushStrategy.push(deviceVO,content);
- } else if(B設(shè)備){
- IotPushStrategy.push(deviceVO,content);
- }
- }
- }
總結(jié):將每種通道的推送邏輯封裝到了具體的策略中,某種策略的變更不會影響其他策略,由于實現(xiàn)了共同接口,所以策略可以互相替換,對使用者友好。
比如 Java ThreadPoolExecutor 中的任務(wù)拒絕策略,當(dāng)線程池已經(jīng)飽和的時候會執(zhí)行拒絕策略,具體的拒絕邏輯被封裝到了 RejectedExecutionHandler 的 rejectedExecution 中。
模板設(shè)計模式
模式定義
模板的價值就在于骨架的定義,骨架內(nèi)部將問題處理的流程已經(jīng)定義好,通用的處理邏輯一般由父類實現(xiàn),個性化的處理邏輯由子類實現(xiàn)。
比如炒土豆絲和炒麻婆豆腐,大體邏輯都是:
- 切菜
- 放油
- 炒菜
- 出鍋
1,2,4 都差不多,但是第 3 步是不一樣的,炒土豆絲得拿鏟子翻炒,但是炒麻婆豆腐得拿勺子輕推,否則豆腐會爛(疫情宅在家,學(xué)了不少菜)。
使用場景
不同場景的處理流程,部分邏輯是通用的,可以放到父類中作為通用實現(xiàn),部分邏輯是個性化的,需要子類去個性實現(xiàn)。
實踐經(jīng)驗
還是接著之前語音播報的例子來說,后期我們新加了兩個需求:
- 消息推送需要增加 trace。
- 有些通道推送失敗需要重試。
所以現(xiàn)在的流程變成了這樣:
- trace 開始。
- 通道開始推送。
- 是否允許重試,如果允許執(zhí)行重試邏輯。
- trace 結(jié)束。
其中 1 和 4 是通用的,2 和 3 是個性化的,鑒于此我在具體的推送策略之前增加了一層父類的策略,將通用邏輯放到了父類中。
修改后的代碼如下:
- abstract class AbstractPushStrategy implements PushStrategy{
- @Override
- public CallResult push(AppDeviceVO deviceVO, Object content) {
- //1.構(gòu)造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結(jié)束
- 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
- }
- }
總結(jié):通過模板定義了流程,將通用邏輯放在父類實現(xiàn),減少了重復(fù)代碼,個性化邏輯由子類自己實現(xiàn),子類間修改代碼互不干擾也不會破壞流程。
觀察者設(shè)計模式
模式定義
顧名思義,此模式需要有觀察者(Observer)和被觀察者(Observable)兩類角色。
當(dāng) Observable 狀態(tài)變化時會通知 Observer,Observer 一般會實現(xiàn)一類通用的接口。
比如 java.util.Observer,Observable 需要通知 Observer 時,逐個調(diào)用 Observer 的 update 方法即可,Observer 的處理成功與否不應(yīng)該影響 Observable 的流程。
使用場景
一個對象(Observable)狀態(tài)改變需要通知其他對象,Observer 的存在不影響 Observable 的處理結(jié)果,Observer 的增刪對 Observable 無感知。
比如 Kafka 的消息訂閱,Producer 發(fā)送一條消息到 Topic,至于是 1 個還是 10 個 Consumer 訂閱這個 Topic,Producer 是不需要關(guān)注的。
實踐經(jīng)驗
在責(zé)任鏈設(shè)計模式那塊我通過三個 Filter 解決了消費限制檢驗的問題,其中有一個 Filter 是用來檢驗消費次數(shù)的,我這里只是讀取用戶的消費次數(shù),那么消費次數(shù)的累加是怎么完成的呢?
其實累加這塊就用到了觀察者模式,具體來講是這樣,當(dāng)交易系統(tǒng)收到支付成功回調(diào)時會通過 Spring 的事件機(jī)制發(fā)布“支付成功事件”。
這樣負(fù)責(zé)累加消費次數(shù)和負(fù)責(zé)語音播報的訂閱者就會收到“支付成功事件”,進(jìn)而做各自的業(yè)務(wù)邏輯。
畫個簡單的圖描述一下:
代碼結(jié)構(gòu)大體如下:
- /**
- 支付回調(diào)處理者
- */
- PayCallBackController implements ApplicationContextAware {
- private ApplicationContext applicationContext;
- //如果想獲取applicationContext需要實現(xiàn)ApplicationContextAware接口,Spring容器會回調(diào)setApplicationContext方法將applicationContext注入進(jìn)來
- @Override
- public void setApplicationContext(ApplicationContext applicationContext)
- throws BeansException {
- this.applicationContext = applicationContext;
- }
- @RequestMapping(value = "/pay/callback.do")
- public View callback(HttpServletRequest request){
- if(paySuccess(request){
- //構(gòu)造支付成功事件
- PaySuccessEvent event = buildPaySuccessEvent(...);
- //通過applicationContext發(fā)布事件,從而達(dá)到通知觀察者的目的
- this.applicationContext.publishEvent(event);
- }
- }
- }
- /**
- * 語音播報處理者
- *
- */
- public class VoiceBroadcastHandler implements ApplicationListener<PaySuccessEvent>{
- @Override
- public void onApplicationEvent(PaySuccessEvent event) {
- //語音播報邏輯
- }
- }
- //其他處理者的邏輯類似
總結(jié):觀察者模式將被觀察者和觀察者之間做了解耦,觀察者存在與否不會影響被觀察者的現(xiàn)有邏輯。
裝飾器設(shè)計模式
模式定義
裝飾器用來包裝原有的類,在對使用者透明的情況下做功能的增強(qiáng),比如 Java 中的 BufferedInputStream 可以對其包裝的 InputStream 做增強(qiáng),從而提供緩沖功能。
使用場景
希望對原有類的功能做增強(qiáng),但又不希望增加過多子類時,可以使用裝飾器模式來達(dá)到同樣的效果。
實踐經(jīng)驗
筆者之前在推動整個公司接入 trace 體系,因此也提供了一些工具來解決 trace 的自動織入和上下文的自動傳遞。
為了支持線程間的上下文傳遞,我增加了 TraceRunnableWrapper 這個裝飾類,從而起到將父線程的上下文透傳到子線程中,對使用者完全透明。
代碼如下:
- /**
- 可以自動攜帶trace上下文的Runnable裝飾器
- */
- public class TraceRunnableWrapper implements Runnable{
- //被包裝的目標(biāo)對象
- private Runnable task;
- private Span parentSpan = null;
- public TraceRunnableWrapper(Runnable task) {
- //1.獲取當(dāng)前線程的上下文(因為new的時候還沒有發(fā)生線程切換,所以需要在這里將上下文獲?。?nbsp;
- //對這塊代碼感興趣的可以查看opentracing API
- io.opentracing.Scope currentScope = GlobalTracer.get().scopeManager().active();
- //2.保存父上下文
- parentSpan = currentScope.span();
- this.task = task;
- }
- @Override
- public void run() {
- //run的時候?qū)⒏妇€程的上下文綁定到當(dāng)前線程
- io.opentracing.Scope scope = GlobalTracer.get().scopeManager().activate(parentSpan,false);
- task.run();
- }
- }
- //使用者
- new Thread(new Runnable(){run(...)}).start()替換為new TraceRunnableWrapper(new Runnable(){run(...)}).start()
總結(jié):使用裝飾器模式做了功能的增強(qiáng),對使用者來說只需要做簡單的組合就能繼續(xù)使用原功能。
外觀設(shè)計模式
模式定義
何為外觀,就是對外提供一個統(tǒng)一的入口:
- 一是可以影藏系統(tǒng)內(nèi)部的細(xì)節(jié)。
- 二是可以降低使用者的復(fù)雜度。
比如 SpringMVC 中的 DispaterServlet,所有的 Controller 都是通過 DispaterServlet 統(tǒng)一暴露。
使用場景
降低使用者的復(fù)雜度,簡化客戶端的接入成本。
實踐經(jīng)驗
筆者所在的公司對外提供了一些開放能力給第三方 ISV,比如設(shè)備管控、統(tǒng)一支付、對賬單下載等能力。
由于分屬于不同的團(tuán)隊,所以對外提供的接口形式各異,初期還好,接口不多,ISV 也能接受,但是后期接口多了 ISV 就開始抱怨接入成本太高。
為了解決這一問題,我們在開放接口前面加了一層前端控制器 GatewayController,其實就是我們后來開放平臺的雛形。
GatewayController 對外統(tǒng)一暴露一個接口 gateway.do,將對外接口的請求參數(shù)和響應(yīng)參數(shù)統(tǒng)一在 GatewayController 做收斂,GatewayController 往后端服務(wù)路由時也采用統(tǒng)一接口。
改造前后對比如下圖:
大概代碼如下:
- 使用者:
- HttpClient.doPost("/gateway.do","{'method':'trade.create','sign':'wxxaaa','timestamp':'15311111111'},'bizContent':'業(yè)務(wù)參數(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()};
- }
- }
總結(jié):采用外觀模式屏蔽了系統(tǒng)內(nèi)部的一些細(xì)節(jié),降低了使用者的接入成本。
就拿 GatewayController 來說,ISV 的鑒權(quán),接口的驗簽等重復(fù)工作統(tǒng)一由它實現(xiàn)。
ISV 對接不同的接口只需要關(guān)心一套接口協(xié)議接口,由 GatewayController 這一層做了收斂。
作者:踩刀詩人
編輯:陶家龍
出處:https://urlify.cn/J3mAna