如何寫出好代碼(一)——抽取模塊別隨意
進入正題前
前端開發是最讓人生畏的,因為你要關心的太多了,調用後臺接口和第三方等的異步操作,核心功能的控制,界面的樣式設計,與用戶交互及提示……稍不留神,代碼就成一團亂麻了。
“哈哈,你說的這些,我早已駕輕就熟了!”
沒錯,但我今天要說的並不是技術,而是編程習慣,設計習慣!
好好看一下你的代碼,問問自己過幾個月再來看,或者現在讓另一個無關的人來看,將花多長時間能看懂呢? 如果要對你的系統的樣式,交互,功能邏輯做一些大的調整,修改和擴充,又需要花多長時間呢,或者你寧願重啟一個項目?
是的,面對一個接一個的開發任務,我們只想著趕快完成,面對一個又一個的bug,我們只想著趕緊將其補起來,卻很少會靜下心來想一想,怎樣讓事情變得更好!
好了,進入正題前再說最後一件事!
其實這篇文章寫不寫都無所謂,因為本文不過是我在踐行前人所說的幾大原則時,所感所想而已,我沒有創造任何東西。
說白了,本文就是結合公司近期的一些典型的開發實踐,找出其中的問題,並給出自己的思考和解決方案,雖然說的是前臺,但其中道理在任何開發中都是適用的。
從故事談抽取模塊
問題的發酵
1. 現在來開發一個簡單的卷庫商品的系統,消費者能正確看到商品列表,每個商品能看到試卷的基本描述字段信息,能展示試卷的試題結構!
經過一段時間開發者實現了這個功能,具體有兩個模塊一個是列表list模塊,控制分頁展示的具體數據的獲取,刷新跳轉等功能。另一個是item模塊,這個模塊負責單個商品的樣式展示,內容控制,點擊交互等。
2. 接下來有另一個需求,用戶會購買試卷,所以有一個我的試卷列表,這個列表也是一個商品展示,和前面基本是一樣的。
看了一下具體的數據結構,我的試卷myPaper 在mongo中冗余了卷庫商品,對應字段是 paper,其他字段是訂單相關,個人相關的信息。這太簡單了,所有的功能都已經開發完了,只需要做一點點修改就行了!
最終的做法如下再加個模塊 my-paper-list 我的試卷列表模塊,和list類似的功能,然後調用item模塊,顯示內容是myPaper.paper就行了。
不過卷庫商品,卷庫訂單 兩個後臺並不是一個人做的,兩個paper字段並不是完全一致的,但這視乎也無傷大雅,只需要在
3.後來又加了一個卷庫推薦paperRcmd的功能,數據結構和myPaper類似,冗余了paper,外加推薦相關的一些信息。於是我們又故技重施了一次,不一樣的地方又做了一些適配處理。
第一階段的開發就這樣完成了,整個過程似乎很順暢,重用性也很高,簡單的邏輯包裝,讓組件適配了三種模型。
4. 但是好景不長,第二個叠代產品經理就想在按鈕上做點事情:列表只有購買按鈕,但有些商品已經購買了,所以應該顯示另外一個按鈕點擊應該進入在線作答,或作答完成顯示查看作答結果按鈕。
目前的設計,好像只能在那個item上下功夫了,好在開發人員,邏輯感還不錯,能夠理清各種關系,最終正確的實現了這個需求!——但是這個原本就包含了三種模型的組件,已經變得很復雜了,為了判斷按鈕,卷庫推薦和卷庫商品都要取一下購買信息,再加上之前做的一些適配,又是參照卷庫商品的,三種模型已經變得是 你中有我,我中有你了。
最終的崩潰
過了好幾個月後,大家認為之前的很多東西是有問題的,比如在顯示優分率信息時是對每個商品額外進行了一次查詢,因為優分率不是固定的,是隨著作答人數和作答情況不斷變化的,這個效率太低,特別是在app端,所以需要改一下,對列表做一個查詢模型。
而且之前的商品有些字段設計的不合理,比如學科字段,以前用的是列表,可是試卷應該只有一個學科。所以應該用普通字段。
在這好幾個月裏,之前的核心開發人員走了兩個,當然還有剩下的。
這次大的修改,查詢模型這一塊是另一個人做的,他對數據結構進行了全新的設計,刪掉了很多無用的結構,將不合理的模型結構進行了調整,這是好事!
因為數據模型改動的太多,頁面很多內容顯示不出來了,這時致命的問題來了,我們發現前端幾乎是沒法修改的,看著那些非常相似卻又完全不同的變量,比如myGoods,goods,rcmdGoods,而且學科,年級這類的字段取值都還有一段又一段的if-else邏輯,不敢改,也不知道怎麽改。
當時的清晰與自信呢?早已沒有了,那不過是因為你對業務非常熟悉,過了這麽久沒有人會清楚的記得每一個細節的,現在在你眼前的不過是亂麻一般的邏輯判斷!開發人員不得不硬著頭皮,憑借自己豐富的開發經驗和判斷力,再加上對業務還是有那麽一點印象,還是能做一些修復工作的(註意是修復,因為這時已經沒有開發的樂趣了,只有自己都不敢保證的壓力),直到真正棘手的問題出現:
詳情裏的學科顯示不出來了,將字段名字改對就好了,但是本來沒出問題的推薦商品的學科,因為你的修改又顯示不出來了。
開發人員最終得出結論:無法修改!除非你願意花時間重新去梳理一下裏面繁雜的邏輯結構,但如果這麽做,還不如做一下重構,將問題徹底解決,也許重構花的時間更少。
思考解決方案
之所以要把這個過程寫的這麽詳細,因為這是真實且普遍存在的,看到這個過程你可能會覺得有很多不合理的,但我們自己往往在不知不覺中就這樣做了!
想一想這個過程,最開始為什麽要拆分,拆成list和item?當然是為了簡單,如果太多事情在一起,就會超出我們控制的範圍。為什麽第二步新的業務要共用,還是為了簡單,共用代碼更容易保持風格的一致,邏輯的一致,修改時更簡單,不用四處尋找。
這就是抽取模塊的兩個根本目的:每個模塊只做一件事,就看起來簡單,很容易被我們掌控;抽取模塊,更容易共用,更容易修改維護代碼,以及保持一致。
上面那個例子做的其實還不錯了,只是沒有做的更徹底。開發人員重構的最終方案是:
卷庫商品,我的試卷,試卷推薦,三個模塊是相互獨立的,都用自己獨立的list和item,因為取數據的方式,路由及參數的方式,字段類似但結構命名等都不太一致,所以應該完全獨立開來,互不幹擾。
另外還有一系列共用組件,主要是些界面組件,因為這三個模塊唯一相同的就是界面展示了,這些共用組件不再接收像商品,我的商品,推薦商品這樣的參數了。比如前面的基礎信息組件,展示的是學科,年級,學制,分數,類型,那麽就只接受學科,年級,學制,分數,類型這些參數,這個組件不用關心,也不應該關心,這些字段來自誰,該怎麽取。不是這樣嗎?
展望一下新方案,未來某個業務比如推薦試卷又有大的改動,大家認為這樣冗余數據的方式不好,不利於數據一致性的維護,想改成關系型的遵循範式的,這個數據結構又要重新設計了,修改起來好像也不難,因為它對應的list和item只有它自己,單一的事情我們總是很自信,因為它足以讓我們確定修改不會顧此失彼。
界面也要做大的改動,當前的界面太傳統太沒創意了,想做的炫一些,沒問題,三個摸塊界面是完全共用的,我們不需要四處修改 還生怕有漏掉的,而且界面模塊只有界面的業務,這修改起來太簡單了。
對比一下新方案和以前的方案,很容易發現其中的關鍵就在於你怎麽定義你的共用組件了。我的理解是共用組件一定要遵循迪米特法則,一個共用組件只需要定義好我需要的參數,以及返回我處理的結果,不應該知道對方是什麽業務,也不應該關心對方怎麽處理我返回的結果。(或者你也可以說抽取組件一定要遵循單一職責原則,失敗案例中的共用組件摻雜了外在業務,比如不同試卷的數據結構和字段取法。這些法則本來就是相通的。)
最後再重復一下,抽取模塊的兩個目的:保持業務簡潔,容易掌控;共用代碼,容易修改!
要遵循的關鍵原則:單一職責原則和迪米特法則。(註意抽取模塊目的不是為了避免重復代碼,不要抽取過度,那樣反而更復雜,一切是為了簡單。)
抽取模塊時我們常犯的錯
上面的例子展現了我們常犯的錯誤,及犯錯誤的過程。其實我們公司的前臺代碼,有很多類似的錯誤,隨處可見。
不遵循迪米特法則,造成業務擴散
這是我們的開發非常常見的問題!!
舉個購買商品服務的例子,我們通常都是這樣定義這個服務的:buy( goods )
具體實現就是取到goods的id,版本號,再取到全局變量userid,就獲取了訪問後臺的全部參數了,然後就執行異步訪問後臺接口的操作,並返回promise。
但這樣定義對修改擴展不夠友好,想象一下,如果要購買的商品有多種類型,比如上面的試卷商品和推薦試卷,難道又要做一次適配嗎?就算只有一種類型,如果某天又對數據機構進行大改,這個服務也必須跟著改,不是嗎?
這個服務不只有自己要實現的功能,還要關心是誰調用我的,怎麽適配各種調用者,未遵循迪米特法則,造成業務擴散。
現在我們這樣定義這個接口,buy( goodsId, goodsVersion), 如果你想做的更絕,讓其通用性更強,可以這樣buy( goodsId, goodsVersion, userId) 因為不同系統 全局變量userid的取法可能不同。結果又會怎樣?
看到了吧,上面的兩種情況擴展和大改,都不需要修改這個服務了,這個服務始終是穩定的——在你遵循了迪米特法則或單一職責法則時,自然而然的就滿足了開閉原則。
如果你覺的這個例子發生的可能性太小,那麽看一下我們的前端代碼吧:寫一個服務,傳入業務實體作為參數,幾乎每個服務都是這麽寫的,很驚訝嗎?
按業務拆分,不按職責拆分,嚴重違背單一職責
用戶交互 與 控制邏輯 相耦合,就是按業務拆分引起的直接後果。
比如說下架一個商品,需要彈出一個確認提示,根據用戶的選擇情況執行操作。如果寫的好點的,大概就是這樣的了,將整體抽取一個服務,服務是這樣的。
public function(goods){
toaster({
……
}).then(function(select){
If(select) down(goods);
});
}
private down(goods){
……
}
開發時我們習慣這樣想,上架商品的功能比復雜,就抽取一個上架商品的服務吧,於是就自然而然的成了這樣。這樣沒問題啊,但界面交互屬於表現view層,上架商品屬於業務處理層,他們屬於兩種職責,不應該在同一個文件中。
也許你覺得我太苛刻了,的確,因為如果你寫的服務只有這一處用到了,這樣是不會出現大問題的,甚至你不抽取服務也是可以的,而且目前我們開發中抽取的服務,絕大部分確實都是只用一次(專用的)!
即使我們的服務都是專用的,這樣寫依然有問題,現在回想一下我們的開發。
彈出什麽樣的框,應該提示什麽,這是產品經理說了算的,而上架的接口調用,是後臺開發者定義的。任何一種變更,這個服務都需要修改,也就是說這個服務是很不穩定的;任何一種的變更,我們都能直接想到應該修改哪裏,是表現層的彈框,還是核心層的實現,但是由於我們將這些代碼寫在一起,每次都要花時間去看一些無關的代碼,需要花更多時間去找到代碼。
沒有人會註意這些細節,因為它引起的麻煩還不足以引起我們的註意。
其實正確的服務應該是這樣的:
private down(goodsId){
……
}
這就完全可以了,至於那個彈框,誰調用就自己定義,本服務根本不用關心,也不應該關心。
想象一下,假如下架商品的交互有多種,有的不需要提示直接下架,有的提示信息不一樣,有的提示後還要再做一些額外操作再下架,這三種情況都比較一下吧!
另外如果不斷嘗試,我們會發現保持這樣做的另一個好處:服務基本是穩定的,而業務基本是整體性的,每一個模塊的controller包含了所有的業務處理,交互處理,界面展現,當然因為你抽取了很多服務和組件,這種整體性的全面是抽象意義上的,整個模塊的結構清晰的一覽無余,卻又非常簡潔和容易掌控。
再來看一下我們的前臺代碼,有多少服務是這樣的?很多人進入了一個誤區,認為拆分就是要將業務打散,但我認為拆分更多的是為了讓業務更抽象,更易於理解,打散業務反而讓系統變得不易維護!
不要再按業務拆分了,按職責,隱藏其實現細節,保持業務的整體性!
總結
原本以為‘如何寫出好代碼’這篇文章不會花太多時間,可是現在用了3天(趁著工作任務不是很多,斷斷續續的寫的)才寫了一半,想表達心中的想法的確不容易,但關於抽取模塊卻已經表達完了,所以這篇文章就這樣命名了!
回頭看這篇文章,基本沒有什麽廢話,裏面的每一句話都是基於我在實際開發中的看到過程,了解到的常見的思考習慣,潛意識心態,以及我自己真實的感受。看了太多的不夠優雅的代碼,經歷過產品需求被反反復復的修改,經歷過後臺業務架構顛覆式的重新設計,嘗過了各種修改困難的苦果,可以說我從沒有間斷過對抽取模塊這個問題的學習、領悟和探索,現在我用最簡單的例子和話語將其表達出來,獻給大家,雖然還是有些枯燥,但還是希望大家能認真讀一下。
但我要強調的,也是最重要的,還是在後續工作時,多在實踐中思考怎麽讓代碼更好,只有實踐中運用和體會,才能真正有所收獲!
如何寫出好代碼(一)——抽取模塊別隨意