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