利用JS職能鏈模式進行小步重構
利用JS職能鏈模式進行小步重構
思考越多,收穫越多。
背景: 業務場景和問題
先前,我在進行H5頁面開發時,曾經有一個業務場景,根據不同的邏輯會在三種彈窗中至多顯示一種彈窗。為了以示區分,假設這三種彈窗,分別是:文字彈窗、圖片彈窗、視訊彈窗。並且,假設優先順序是:文字彈窗 > 圖片彈窗 > 視訊彈窗(產品同學為了照顧使用者可貴的流量,先用低流量的文字,到最後高流量的視訊)。
經抽離關鍵程式碼後,一開始的程式碼實現大概如下:
var h5 = { init: function() { // 彈出優先順序 : 文字 - 圖片 - 視訊 h5.popupText(); }, /* * 文字彈窗 */ popupText: function() { api.getText().done(function(re) { if (re.code == 1) { // 顯示文字彈窗 ... } }).always(function(re = {}) { if (re.code != 1) { h5.popupImg(); // 繼續圖片彈窗 } }); }, /* * 圖片彈窗 */ popupImg: function() { api.getImg().done(function(re) { if (re.code == 1) { // 顯示圖片彈窗 ... } }).always(function(re = {}) { if (re.code != 1) { h5.popupVideo(); // 繼續視訊彈窗 } }); }, /* * 視訊彈窗 */ popupVideo: function() { // 顯示視訊彈窗 ... } }
上面的程式碼雖然有點長,但還是不難看出其中的業務邏輯的。
這裡的問題是,重要的業務規則得不到很好的體現,並且複雜的業務場景實現沒有使用通用的、既有的模式得到恰當的解決。
職能鏈簡介
這裡打算使用職能鏈模式進行重構優化,關於職能鏈模式的靜態UML結構如下:
這裡不過多講述職能鏈模式的說明,感興趣的同學可自行百度。舉一個員工的問題為例子,假設有一名基層員工遇到問題,他解決不了,然後去找他的老大;如果他老大也解決不了的話,就會繼續再往上一級找老大的老大,依次類推,直到問題被解決。這就是職能鏈模式。
重構後的程式碼
經過一番改造後,使用職能鏈模式重構後的程式碼,主要如下。
var h5 = { init: function() { // 彈窗職能鏈呼叫 var chain = new MiniChain(); chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go(); }, /* * 文字彈窗 */ popupText: function() { var isShow = false; api.getText().done(function(re) { if (re.code == 1) { // 顯示文字彈窗 ... isShow = true; } else { // 其他場景處理 ... } return re; }); return isShow; } }, /* * 圖片彈窗 */ popupImg: function() { var isShow = false; api.getImg().done(function(re) { if (re. switch == 1) { isShow = true; // 圖片彈窗顯示 } }); return isShow; }, /* * 視訊彈窗 */ popupVideo: function() { // 顯示視訊彈窗 ... } }
主要的改動,首先,是職能鏈的按優先順序的配置和排程:
// 彈窗職能鏈呼叫 var chain = new MiniChain(); chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go();
其次,是各職能鏈(在這裡是各彈窗功能)的具體實現,主要添加了處理標識返回,但內部實現已經更專注於自身功能的實現,而非外部的判斷和處理。
重構後的好處
這樣重構後,明顯我們看到核心關鍵的業務得到了很好的突顯,並且把業務規則通過恰當的模式組合最終恰如其分地表達了出來。下面再細說下重構後的好處。
好處1:關注點分離,排程與實現分離
排程的策略,應該和具體的彈窗功能實現分開。這是兩個不同的關注點,如何排程,是高層的概念;而具體的實現則是技術細節的範疇,不應把這兩者混淆。一旦混淆了,就會容易產生程式碼異味。
重構前,排程的入口是由文字彈窗的呼叫而開始的。這樣很容易會給人一種誤導,或者是隱藏了重要的資訊。因為只有深入到細節,才知道當沒有文字彈窗時才會繼續嘗試彈圖片彈窗。更讓人“驚訝”的是,文字彈窗沒顯示時會再嘗試視訊彈窗。。。 這裡沒有很好地在高層表達重要的資訊,而且也沒有很好的抽象高層的業務概念。這個入口,就如同一個黑黑的洞穴,只有你拿著火把,一步步深入其中,走到底,才知道還有沒有路可走。
而重構後,我們把排程分離了出來,並用貼切的職能鏈模式表現了出來。高層的業務概念不僅得到了體現,而各自的彈窗功能實現也變得了更內聚(因為不需要再關注要不要再走下一步),這也是符合我們常常說的“高內聚、低耦合”。
好處2:對業務友好、對開發友好、對測試友好
對業務友好
回顧前面彈窗優先順序的配置程式碼:
// 彈窗職能鏈呼叫 var chain = new MiniChain(); chain.next(h5.popupText).next(h5.popupImg).next(h5.popupVideo).go();
可以看到,對於彈窗功能的優先順序順序編排非常簡單明瞭,因此當產品需要增加彈窗、刪除彈窗、調整順序時都非常簡、快速,快速即友好,因為能快速滿足產品同學的需求,快速交付有價值的功能。
對開發友好
除了快速外,它也是容易維護,即維護成本低。
因為重構後,把原來的三層巢狀呼叫簡化了只有一層巢狀,我把這種調整稱之為巢狀結構扁平化。扁平化的各個彈窗功能模組,可以獨立重用、互不影響,通用職能鏈組合複用起來,而不是像之前那樣“暗地裡”耦合在一起。維護成本變低的原因還有技術層面的,因為通過配置的彈窗優先順序,我們不僅可以看到當前已有多少彈窗以便評估效能方面對使用者體驗的影響,還可以方便判斷是否存在死迴圈、消除複雜的耦合關係。在這裡,又讓我再一次想起了那段話:
“設計軟體有兩種方法:一種是簡單到明顯沒有缺陷,另一種複雜到缺陷不那麼明顯。”
舉個假設的需求以對比重構前後的維護成本。
假設,產品希望原來把彈窗優先順序,從原來的:文字彈窗 -> 圖片彈窗 -> 視訊彈窗。調整成(全部逆轉):視訊彈窗 -> 圖片彈窗 -> 文字彈窗 。
試想一下,如果是原來的方式,需要開發的時間是多少?1分鐘、5分鐘、10分鐘、半小時?其中還需要進行開發自測。這裡可能維護時間因人而異,但我覺得,負責維護的開發工程師至少也要想一下。
但如果是重構後,我覺得,即便是剛畢業的實習生,也能夠在短短的10秒鐘內完成開發(並且可以不用自測!)。因為需要修改的程式碼就是這麼簡單:
// 彈窗職能鏈呼叫 var chain = new MiniChain(); chain.next(h5.popupVideo).next(h5.popupText).next(h5.popupImg).go();
在這裡,對於開發工程師,U維護成本低即友好**。
對測試友好
很多因素,雖然微弱,雖然看似無關聯,實際上相互聯絡、相互作用的。從程式碼上的修改,反映到對產品需求的響應、再到維護成本的影響,最後也自然會影響到測試。重構後的程式碼,質量上得到了提升,也就得到了保障,對於測試人員,不必要擔心修改後有問題而花過多時間去迴歸功能或者改出問題後進行故障的登記和跟進。另外,對比原來的測試路徑,可以有 2*2*2=8 種組合場景;而扁平化巢狀結構後,測試只需要測試 2+2+2 = 6種獨立的場景即可,可以少測3種場景。因為原來排列組合的方式,現在只需要單獨測試每個模組即可。
在這裡,對於測試工程師,減輕測試工作量即友好。
職能鏈的考慮: 明確成功後終止,抑或明確失敗後繼續?
在實現職能鏈模式時,需要考慮的一個小細節就是:到底是明確成功後終止,抑或明確失敗後繼續?
這裡涉及到何時停止的裁定,所以在JS實現時是需要仔細考慮的。一開始是返回false才會繼續的,但由於對於彈窗的業務功能實現時,開發工程師有可能返回undefine、或者null、或者0等其他類似false的場景,但我們使用了全等判斷,故而也就會對開發工程師嚴格要求,否則就會容易產生bug。為了繼續體現對開發者的友好性(幫助開發工程師減少出錯的概率),我決定最後調整為寬鬆的約定,即你明確告訴我成功了才會停止排程,否則當作失敗繼續排程下一個處理器。
小遊戲:誰是內奸
假設有這麼一個遊戲:找出警隊裡的內奸。某警隊裡有3箇中隊,分別有3人、4人、5人,其中有一個是內奸,需要FBI聯邦總局命令你找出內奸。請設計合適的演算法,找出內奸。
這裡的問題,如果用圖論中的演算法結構,我們可以得到這樣的樹模型(假設內奸位置已按演算法分配好):
利用上面剛學到的職能鏈,我發現可以這樣來處理。
// 小遊戲:誰是內奸 QUnit.test("little game: WHO IS SPY", function (assert) { var i_am_police = function () { console.log("I am police."); return false; } var i_am_spy = function () { console.log("I am spy!"); return true; } var team_1 = new MiniChain(); team_1.next(i_am_police).next(i_am_police).next(i_am_police); var team_2 = new MiniChain(); team_2.next(i_am_police).next(i_am_police).next(i_am_spy).next(i_am_police); // 內奸在這! var team_3 = new MiniChain(); team_3.next(i_am_police).next(i_am_police).next(i_am_police).next(i_am_police).next(i_am_police); var chain = new MiniChain(); chain.next(team_1.go).next(team_2.go).next(team_3.go); var rs = chain.go(); assert.deepEqual(rs, true); // 找到內奸 });
執行的效果如下(期望效果):
LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am police.' LOG: 'I am spy!'
很遺憾的是,目前上面的程式碼執行還是有問題的。看來暫時還不宜找出內奸。 這裡明顯雖然不一定要用職能鏈模式,而職能鏈模式也不是構建樹最好的解決方案,但通過這種的分解後,我們可以得到的是可重用、可複用的獨立模組,進而組合產生出你所想要的功能。再強調一次:複用而非耦合。
不適應性場景: 適合於操作類功能,不適宜返回結果
任何負責任的技術或者思想,都應該告訴你其不適應性。同樣,就這裡的JS職能鏈,也有其不適應性。它不適合用於返回結果的功能,因為我們約定了返回true作為成功授理的標識,否則會再進行下一步。
當然,也可以根據場景需要,改造成帶返回結果的職能鏈。但我覺得,模式也應該是專注的,即職能鏈本質就適合於操作類不帶結果返回的實現。
小結
最後,模式不要濫用,僅當需要時才用。實現細節不要過於生搬硬套。重要是思想的體現。
轉載於:https://my.oschina.net/dogstar/blog/867384