代碼越寫越亂?那是因?yàn)槟銢]用責(zé)任鏈
目的
在開始學(xué)習(xí)責(zé)任鏈之前,先看一下在開發(fā)中常見的問題。下面是前端用來處理 API 錯(cuò)誤碼的代碼:
- const httpErrorHandler = (error) => {
- const errorStatus = error.response.status;
- if (errorStatus === 400) {
- console.log('你是不是提交了什么奇怪的東西?');
- }
- if (errorStatus === 401) {
- console.log('需要先登陸!');
- }
- if (errorStatus === 403) {
- console.log('是不是想偷摸干壞事?');
- }
- if (errorStatus === 404) {
- console.log('這里什么也沒有...');
- }
- };
當(dāng)然實(shí)際項(xiàng)目中不可能只有一行 console,這是為了說明原理的簡化版。
代碼中的 httpErrorHandler 會(huì)接收 API 的響應(yīng)錯(cuò)誤,并對(duì)錯(cuò)誤的狀態(tài)碼做不同的處理,所以代碼中需要很多 if(或者 switch)判斷當(dāng)前需要要執(zhí)行什么,當(dāng)你要對(duì)新的錯(cuò)誤添加處理代碼時(shí),就必須要到 httpErrorHandler 中修改代碼。
雖然免不了要經(jīng)常修改代碼,但是這樣做可能會(huì)導(dǎo)致幾個(gè)問題,下面根據(jù) SOLID 的 單一職責(zé)(Single responsibility)和開放封閉(open/close)這兩個(gè)原則來說明:
單一職責(zé)(Single responsibility)
簡單的說,單一職責(zé)就是只做一件事情。而前面的 httpErrorHandler 方法以使用的角度來說,是把錯(cuò)誤對(duì)象交給它,讓它按照錯(cuò)誤碼做對(duì)應(yīng)的處理。看上去好像是在做“錯(cuò)誤處理”這個(gè)單一的事情,但是從實(shí)現(xiàn)的角度上來說,它把不同錯(cuò)誤的處理邏輯全部寫在了 httpErrorHandler 中,這就會(huì)導(dǎo)致可能在只想要修改對(duì)錯(cuò)誤碼為 400 的邏輯時(shí),但是不得不閱讀一大堆不相關(guān)的代碼。
開放封閉原則(open/close)
開放封閉原則是指對(duì)已經(jīng)寫好的核心邏輯就不要再去改動(dòng),但同時(shí)要能夠因需求的增加而擴(kuò)充原本的功能,也就是開放擴(kuò)充功能,同時(shí)封閉修改原本正確的邏輯。再回過頭來看 httpErrorHandler,如果需要增加一個(gè)對(duì)錯(cuò)誤碼 405 的處理邏輯(要擴(kuò)充新功能),那就需要修改 httpErrorHandler 中的代碼(修改原本正確的邏輯),這也很容易造成原來正確執(zhí)行的代碼出錯(cuò)。
既然 httpErrorHandler 破綻這么多,那該怎么辦?
解決問題
分離邏輯
先讓 httpErrorHandler 符合單一原則。首先把每個(gè)錯(cuò)誤的處理邏輯分別拆成方法:
- const response400 = () => {
- console.log('你是不是提交了什么奇怪的東西?');
- };
- const response401 = () => {
- console.log('需要先登陸!');
- };
- const response403 = () => {
- console.log('是不是想偷摸干壞事?');
- };
- const response404 = () => {
- console.log('這里什么也沒有...');
- };
- const httpErrorHandler = (error) => {
- const errorStatus = error.response.status;
- if (errorStatus === 400) {
- response400();
- }
- if (errorStatus === 401) {
- response401();
- }
- if (errorStatus === 403) {
- response403();
- }
- if (errorStatus === 404) {
- response404();
- }
- };
雖然只是把每個(gè)區(qū)塊的邏輯拆成方法,但這已經(jīng)可以讓我們在修改某個(gè)狀態(tài)碼的錯(cuò)誤處理時(shí),不用再到 httpErrorHandler 中閱讀大量的代碼了。
僅僅是分離邏輯這個(gè)操作同時(shí)也讓 httpErrorHandler 符合了開放封閉原則,因?yàn)樵诎彦e(cuò)誤處理的邏輯各自拆分為方法的時(shí)候,就等于對(duì)那些已經(jīng)完成的代碼進(jìn)行了封裝,這時(shí)當(dāng)需要再為 httpErrorHandler 增加對(duì) 405 的錯(cuò)誤處理邏輯時(shí),就不會(huì)影響到其他的錯(cuò)誤處理邏輯的方法(封閉修改),而是另行創(chuàng)建一個(gè)新的 response405 方法,并在 httpErrorHandler 中加上新的條件判斷就行了(開放擴(kuò)充新功能)。
現(xiàn)在的 httpErrorHandler 其實(shí)是策略模式(strategy pattern),httpErrorHandler 用了統(tǒng)一的接口(方法)來處理各種不同的錯(cuò)誤狀態(tài),在本文的最后會(huì)再次解釋策略模式和責(zé)任鏈之間的區(qū)別。
責(zé)任鏈模式(Chain of Responsibility Pattern)責(zé)任鏈的實(shí)現(xiàn)
原理很簡單,就是把所有方法串起來一個(gè)一個(gè)執(zhí)行,并且每個(gè)方法都只做自己要做的事就行了,例如 response400 只在遇到狀態(tài)碼為 400 的時(shí)候執(zhí)行,而 response401 只處理 401 的錯(cuò)誤,其他方法也都只在自己該處理的時(shí)候執(zhí)行。每個(gè)人各司其職,就是責(zé)任鏈。
接下來開始實(shí)現(xiàn)。
增加判斷
根據(jù)責(zé)任鏈的定義,每個(gè)方法都必須要知道當(dāng)前這件事是不是自己應(yīng)該處理的,所以要把原本在 httpErrorHandler 實(shí)現(xiàn)的 if 判斷分散到每個(gè)方法中,變成由內(nèi)部控制自己的責(zé)任:
- const response400 = (error) => {
- if (error.response.status !== 400) return;
- console.log('你是不是提交了什么奇怪的東西?');
- };
- const response401 = (error) => {
- if (error.response.status !== 401) return;
- console.log('需要先登陸!');
- };
- const response403 = (error) => {
- if (error.response.status !== 403) return;
- console.log('是不是想偷摸干壞事?');
- };
- const response404 = (error) => {
- if (error.response.status !== 404) return;
- console.log('這里什么也沒有...');
- };
- const httpErrorHandler = (error) => {
- response400(error);
- response401(error);
- response403(error);
- response404(error);
- };
把判斷的邏輯放到各自的方法中之后,httpErrorHandler 的代碼就精簡了很多,也去除了所有在 httpErrorHandler 中的邏輯,現(xiàn)在httpErrorHandler 只需要按照順序執(zhí)行 response400 到 response404 就行了,反正該執(zhí)行就執(zhí)行,不該執(zhí)行的也只是直接 return 而已。
實(shí)現(xiàn)真正的責(zé)任鏈
雖然只要重構(gòu)到上一步,所有被分拆的錯(cuò)誤處理方法都會(huì)自行判斷當(dāng)前是不是自己該做的,但是如果你的代碼就這樣了,那么將來看到 httpErrorHandler 的其他人只會(huì)說:
這是什么神仙代碼?API 一遇到錯(cuò)誤就執(zhí)行所有錯(cuò)誤處理?
因?yàn)樗麄儾恢涝诿總€(gè)處理方法里面還有判斷,也許過一段時(shí)間之后你自己也會(huì)忘了這事,因?yàn)楝F(xiàn)在的 httpErrorHandler 看起來就只是從 response400 到 response404,即使我們知道功能正確,但完全看不出是用了責(zé)任鏈。
那到底怎樣才能看起來像是個(gè)鏈呢?其實(shí)你可以直接用一個(gè)數(shù)字記錄所有要被執(zhí)行的錯(cuò)誤處理方法,并通過命名告訴將來看到這段代碼的人這里是責(zé)任鏈:
- const httpErrorHandler = (error) => {
- const errorHandlerChain = [
- response400,
- response401,
- response403,
- response404
- ];
- errorHandlerChain.forEach((errorHandler) => {
- errorHandler(error);
- });
- };
優(yōu)化執(zhí)行
這樣一來責(zé)任鏈的目的就有達(dá)到了,如果像上面代碼中用 forEach 處理的話,那當(dāng)遇到 400 錯(cuò)誤時(shí),實(shí)際上是不需要執(zhí)行后面的 response401 到 response404 的。
所以還要在每個(gè)錯(cuò)誤處理的方法中加上一些邏輯,讓每個(gè)方法可以判斷,如果是遇到自己處理不了的事情,就丟出一個(gè)指定的字符串或布爾值,接收到之后就再接著執(zhí)行下一個(gè)方法,但如果該方法可以處理,則在處理完畢之后直接結(jié)束,不需要再繼續(xù)把整個(gè)鏈跑完。
- const response400 = (error) => {
- if (error.response.status !== 400) return 'next';
- console.log('你是不是提交了什么奇怪的東西?');
- };
- const response401 = (error) => {
- if (error.response.status !== 401) return 'next';
- console.log('需要先登陸!');
- };
- const response403 = (error) => {
- if (error.response.status !== 403) return 'next';;
- console.log('是不是想偷摸干壞事?');
- };
- const response404 = (error) => {
- if (error.response.status !== 404) return 'next';;
- console.log('這里什么都沒有...');
- };
如果鏈中某個(gè)節(jié)點(diǎn)執(zhí)行結(jié)果為 next,則讓下后面的方法繼續(xù)處理:
- const httpErrorHandler = (error) => {
- const errorHandlerChain = [
- response400,
- response401,
- response403,
- response404
- ];
- for(errorHandler of errorHandlerChain) {
- const result = errorHandler(error);
- if (result !== 'next') break;
- };
- };
封裝責(zé)任鏈的實(shí)現(xiàn)
現(xiàn)在責(zé)任鏈已經(jīng)實(shí)現(xiàn)完成了,但是判斷要不要給下一個(gè)方法的邏輯(判斷 result !== 'next') ,卻暴露在外面,這也許會(huì)導(dǎo)致項(xiàng)目中每個(gè)鏈的實(shí)現(xiàn)方法都會(huì)不一樣,其他的鏈有可能是判斷 nextSuccessor 或是 boolean,所以最后還需要封裝一下責(zé)任鏈的實(shí)現(xiàn),讓團(tuán)隊(duì)中的每個(gè)人都可以使用并且遵守項(xiàng)目中的規(guī)范。
責(zé)任鏈需要:
- 當(dāng)前的執(zhí)行者。
- 下一個(gè)的接收者。
- 判斷當(dāng)前執(zhí)行者執(zhí)行后是否需要交由下一個(gè)執(zhí)行者。
所以封裝成類以后應(yīng)該是這樣:
- class Chain {
- constructor(handler) {
- this.handler = handler;
- this.successor = null;
- }
- setSuccessor(successor) {
- this.successor = successor;
- return this;
- }
- passRequest(...args) {
- const result = this.handler(...args);
- if (result === 'next') {
- return this.successor && this.successor.passRequest(...args);
- }
- return result;
- }
- }
用 Chain 創(chuàng)建對(duì)象時(shí)需要將當(dāng)前的職責(zé)方法傳入并設(shè)置給 handler,并且可以在新對(duì)象上用 setSuccessor 把鏈中的下一個(gè)對(duì)象指定給 successor,在 setSuccessor 里返回代表整條鏈的 this,這樣在操作的時(shí)候可以直接在 setSuccessor 后面用 setSuccessor 設(shè)置下一個(gè)接收者。
最后,每個(gè)通過 Chain 產(chǎn)生的對(duì)象都會(huì)有 passRequest 來執(zhí)行當(dāng)前的職責(zé)方法,…arg 會(huì)把傳入的所有參數(shù)變成一個(gè)數(shù)組,然后一起交給 handler 也就是當(dāng)前的職責(zé)方法執(zhí)行,如果返回的結(jié)果 result 是 next 的話,就去判斷有沒有指定 sucessor 如果有的話就繼續(xù)執(zhí)行,如果 result 不是 next,則直接返回 result。
有了 Chain 后代碼就會(huì)變成:
- const httpErrorHandler = (error) => {
- const chainRequest400 = new Chain(response400);
- const chainRequest401 = new Chain(response401);
- const chainRequest403 = new Chain(response403);
- const chainRequest404 = new Chain(response404);
- chainRequest400.setSuccessor(chainRequest401);
- chainRequest401.setSuccessor(chainRequest403);
- chainRequest403.setSuccessor(chainRequest404);
- chainRequest400.passRequest(error);
- };
這時(shí)就很有鏈的感覺了,大家還可以再繼續(xù)根據(jù)自己的需求做調(diào)整,或是也不一定要使用類,因?yàn)樵O(shè)計(jì)模式的使用并不需要局限于如何實(shí)現(xiàn),只要有表達(dá)出該模式的意圖就夠了。
責(zé)任鏈的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
符合單一職責(zé),使每個(gè)方法中都只有一個(gè)職責(zé)。
符合開放封閉原則,在需求增加時(shí)可以很方便的擴(kuò)充新的責(zé)任。
使用時(shí)候不需要知道誰才是真正處理方法,減少大量的 if 或 switch 語法。
缺點(diǎn):
團(tuán)隊(duì)成員需要對(duì)責(zé)任鏈存在共識(shí),否則當(dāng)看到一個(gè)方法莫名其妙的返回一個(gè) next 時(shí)一定會(huì)很奇怪。
出錯(cuò)時(shí)不好排查問題,因?yàn)椴恢赖降自谀膫€(gè)責(zé)任中出的錯(cuò),需要從鏈頭開始往后找。
就算是不需要做任何處理的方法也會(huì)執(zhí)行到,因?yàn)樗谕粋€(gè)鏈中,文中的例子都是同步執(zhí)行的,如果有異步請求的話,執(zhí)行時(shí)間也許就會(huì)比較長。
與策略模式的不同
在前面我還提到過策略模式,先說說兩個(gè)模式之間的相似處,那就是都可以替多個(gè)同一個(gè)行為(response400、response401 等)定義一個(gè)接口(httpErrorHandler),而且在使用時(shí)不需要知道最后是誰執(zhí)行的。在實(shí)現(xiàn)上策略模式比較簡單。
由于策略模式直接用 if 或 switch 來控制誰該做這件事情,比較適合一個(gè)蘿卜一個(gè)坑的狀況。而策略模式雖然在例子中也是針對(duì)錯(cuò)誤的狀態(tài)碼做各自的事,都在不歸自己管的時(shí)候直接把事交給下一位處理,但是在責(zé)任鏈中的每個(gè)節(jié)點(diǎn)仍然可以在不歸自己管的時(shí)候先做些什么,然后再交給下個(gè)節(jié)點(diǎn):
- const response400 = (error) => {
- if (error.response.status !== 400) {
- // 先做點(diǎn)什么...
- return 'next';
- }
- console.log('你是不是提交了什么奇怪的東西?');
- };
那在什么場景下使用呢?
比如在離職時(shí)需要走一個(gè)簽字流程:你自己、你的 Leader 還有人資都需要做簽字這件事,所以責(zé)任鏈就可以把這三個(gè)角色的簽字過程串成一個(gè)流程,每個(gè)人簽過后都會(huì)交給下面一位,一直到人資簽完后才完成整個(gè)流程。而且如果通過責(zé)任鏈處理這個(gè)流程,不論之后流程怎樣變動(dòng)或增加,都有辦法進(jìn)行彈性處理。
上面的需求是策略模式所無法勝任的。