實戰經驗 | 怎樣才能提升代碼質量?

簡介:提升代碼質量的三個有效方法:領域建模、設計原則、設計模式。

影響代碼差的根因

差代碼的體現

我們可以列舉出非常多質量差的代碼的表現現象,如名字不知所意、超大類、超大方法、重複代碼、代碼難懂、代碼修改困難……其中最爲影響代碼質量的兩個表現是命名名不副實、邏輯可擴展性差,當一個新人閱讀代碼時,有時發現方法命名與實際邏輯對不上,這就讓人感到非常疑惑,這種現象在平時工作並不少見;另一個就是邏輯擴展性差,一個新業務需求提出來後,發現要在多處改動,需要回歸的業務邏輯比較多,造成研發效率不高。

問題歸納

對前文提到的現象進行問題歸納整理,大致整理出 6 類問題,分別展開加以說明。

  • 命名問題:命名問題是一件非常頭疼的事,想要取一個名副其實又好理解的名字並不那麼容易。涉及到變量的命名、方法的命名、類命名,常見的命名問題有兩種:一種是不知所云;另一種是名不副實。命名不知所云是一個人初一看,不知道它是什麼意思,根本原因就是沒有想到一個合適的詞彙去抽象問題;命名名不副實是命名和實際邏輯想表達的意思不一樣,這樣的命名會誤導人。
  • 代碼結構問題:當一個人初看工程代碼時,當還沒有深入看代碼邏輯時,從模塊劃分、類劃分、方法劃分整體上可以感受得出代碼質量,如果一個類有幾千行代碼,一個方法有幾百行,這樣的邏輯相信沒有多少人願意去看,複雜度比較高。好的代碼層次結構非常清晰,就像看一本優美的書一樣有一種賞心悅目的感覺。
  • 編程範式問題:有三種編程範式:表模式、事務腳本模式和領域設計模式,大家用得最多的是事務腳本模式,這種模式最符合人做事的方法,step by step,這種模式最大的問題就是承擔了不該自己承擔的職責,看起來比較符合邏輯,實際上問題比較多,平時大家喜歡稱之爲 "麪條型代碼"。
  • 可讀性問題:代碼除了實現業務功能外,還要具備良好的可讀性,有的代碼沒有任何註釋;有的代碼格式不統一;有的是爲了炫耀技術,大段大段的 Lambda 表達式(並不是說 Lambda 表達式不好,關鍵要控制層次深度),這樣的代碼看起來簡潔,可讀性並不太好。
  • 擴展性問題:可擴展性問題是一個老生常談的問題,要實現良好的可擴展性並不那麼容易,一般是沒有抽象問題,如店鋪在店招頭展示 Tab,麪條型的代碼就是直接定義一個 List,然後往裏面加 Tab 對象,如果需要再加一個 Tab 怎麼辦?典型的就是不滿足開閉原則。
  • 無設計問題:整個代碼看起來比較平淡,別人看了之後也從中學習不到內容。一般這種問題是沒有深入分析問題,僅僅解決了問題,而沒有考慮如何更好地解決問題,比如重複處理流程的工作是否可以抽象成一個通用的模板類、不同處理類是否可以通過工廠類去獲取具體的策略、異步處理是否可以使用事件模式去處理、對於新增加的能力能否通過自動註冊去發現……

根因分析

接下來分析下爲什麼會產生代碼差的原因,這個問題有外部原因,也有內部原因。

  • 外部原因主要有:項目排期急,沒有多少時間去設計;資源短缺,人手不夠,只能怎麼快怎麼來;緊急問題修復,臨時方案快速處理……。
  • 內部原因主要有:自身技能低,怎麼技能沒有掌握到,如 Lamda 表達式、常用的工具類、框架高級用法等;無極致追求的精神,僅僅完成需求就行,穩定性、可擴展性、性能、數據一致性等沒有考慮……

筆者認爲最爲關鍵的是內部自身的問題,根因就兩個:自我要求不高;無反饋通道。如果對自已要求不高,僅僅滿足完成需求開發就止步了,很難寫出高質量的代碼,另外如果沒有外部反饋,也難以提高自己的技能。筆者之前的主管非常嚴厲,對大家寫的代碼 review 比較仔細,一個變量名、一段邏輯的寫法,反覆讓修改,這其實是提升技能最快的方法。

提升代碼質量的方法

提升代碼質量的方法,作者喜歡用三個方法:領域建模、設計原則、設計模式,主要談下如何使用:

  • 分析階段:當拿到一個需求時,先不要着急想着怎麼把這個功能實現,這種很容易陷入事務腳本的模式。分析什麼呢?需要分析需求的目的是什麼、完成該功能需要哪些實體承擔,這一步核心是找實體。舉個上面進店 Tab 展示的例子,它有兩個關鍵的實體:導航欄、Tab,其中導航欄裏面包含了若干個 Tab。
  • 設計階段:分析完了有哪些實體後,再分析職責如何分配到具體的實體上,這就要運用一些設計原則去指導,GRASP 中提到一些職責分配的原則,感興趣的同學可以去詳細看看。回到上面的例子上,Tab 的職責主要有兩個:一個是 Tab 能否展示,這是它自己的職責,如上新 Tab 展示的邏輯是店鋪 30 天內有上架新商品;另一個職責就是 Tab 規格信息的構建,也是它自己要負責的。導航欄的職責有兩個:一個是接受 Tab 註冊;另一個是展示。職責分配不適理,也就不滿足高內聚、低耦合的特徵。
  • 打磨階段:這個階段選擇合適的模式去實現,大家一看到模式都會理解它是做什麼的,比如看到模板類,就會知道處理通用的業務流程,具體變化的部分放在子類中處理。上面的這個例子,用到了 2 個設計模式:一個是訂閱者模式, Tab 自動註冊的過程;另一個是模板模式,先判斷 Tab 能否展示,然後再構建 Tab 規格信息,流程雖然簡單,也可以抽象出來通用的流程出來,子類只用簡單地重寫 2 個方法。

領域模型的作用

領域建模的入門門檻比較高,包含了一些難理解的概念。本篇文章中並不會講述如何進行建模(可以私下交流),筆者發現讓大家接受領域建模遠比知道如何建模更重要,當你知道了領域建模的作用後,自己會想各種辦法去學習。下面通過筆者經歷的一些實際案例進行闡述,讓大家聽起來並不感覺到那麼空洞。

簡化認識

筆者工作一年後加入到了一家金融公司,當時對金融一無所知,開始接觸到標的、債權、債權轉讓、融資擔保、非融資擔保等名詞後,一時感到無所適從,每天要學習非常多的新內容。

兩個月後,我的主管給我們做了一次分享,就拿了一張 ppt 來講,它裏面包含了領域的實體,以及實體之間的關聯關係,一下子我就知道了整個業務是怎麼玩轉的。模型的作用就是簡化人對事物的認識,如果一開始我們就陷入到代碼細節中,很難看到業務的全貌,而且代碼是爲了實現業務能力,當你知道了業務之後,再去看代碼就會快得多。

統一認識

在公司裏,有研發、產品、運營、測試……,當我們在一起交流的時候,大家默認的語言是不統一的,開發經常講怎麼操作這張數據庫表,產品經常講業務模式……這就導致大家的認識並不統一。

那是一個晚上,剛和交互同學確認完交互流程後,突然她問了一個問題:把相似的頁面讓賣家移到同一個文夾中,這個好實現吧?聽完後告知不能,交互同學一聽說這很合理呀,怎麼實現不了?開始給她講了下現有的系統流程,發現她聽得一臉懵,我立刻意識到,我是用開發的語言在描述問題,立馬換了一種方式,找了一支筆和一張紙,給交互同學畫了我們的領域模型是什麼,業務實體之間的交互是怎樣的,一講完後,交互同學馬上明白了爲什麼不能實現的原因所在了。

指導設計

有的同學覺得領域建模偏空洞,比較虛,其實除了能夠簡化認識和統一認識外,領域建模還可能指導代碼設計,比如上面舉的店鋪導航 Tab 的例子,筆者就是通過領域建模來設計的,雖然它是一個小的需求,並不妨礙領域建模的運用。

在下圖中,可以清晰的看到,導航欄包含了若干個 Tab,一個 Tab 包含規格信息和點擊操作信息。把這個業務模式畫出來之後,對應的代碼中也會有上面的概念,現實與代碼之間存在映射關係,模型即代碼,代碼即模型。如果你的模型不能反映現實,模塊只能算是一個花架子,範鋼老師對此總結了三句話:現實有什麼事物,對應有什麼對象;現實事物有什麼行爲,對應對象有什麼方法;現實事物有什麼聯繫,對應對象有什麼關聯。

設計原則的底層邏輯

SOLID

對於設計原則,一般我們會談到 SOLID,它包含了五個設計原則:

  • 單一職責原則:A class should have one, and only one, reason to change,一個類只能因爲一個理由被修改。
  • 開閉原則:Entities should be open for extension, but closed for modification,對擴展開放,對修改關閉。
  • 里氏替換原則:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it,子類可以替換父類。
  • 接口隔離原則:A client should not be forced to implement an interface that it doesn’t use,不能強制客戶端實現它不使用的接口,應該把接口拆的儘可能小。
  • 依賴倒置原則:Abstractions should not depend on details. Details should depend on abstractions,抽象不依賴於細節,而細節依賴於抽象。

爲什麼要有設計原則

我們對 SOLID 原則基本上聽說過或者瞭解過,但爲什麼要有這些設計原則呢?爲了回答這個問題,我們從目標往下推導下。軟件開發的目標是高內聚、低耦合,這句掛在嘴邊的話,發現很難衡量,比如要回答:什麼樣的叫高內聚?什麼樣的叫低耦合?高內聚要高到什麼程度?低耦合要低到什麼程度?這四個問題並不太好回答。

反過來想想,如果我們的代碼不是高內聚和低耦合的會怎樣?也即是低內聚和高耦合的場景。如果代碼是低內聚和高耦合,則會出現修改一個邏輯,會導致多處代碼要修改,這個並不是我們希望看到的,尤其在修改原有的邏輯,很容易出現 bug,比如筆者之前修改一個問題,改了另外一處的規則,看起來是沒有問題,結果影響到了一個業務方,這也是爲什麼開閉原則提出對修改關閉的原因,修改原有的邏輯是有風險的。

理想的情況是修改只限定在某個局部範圍內,這樣影響的範圍有限,因此我們要求邏輯要單一,不要包含多個職責。再往下思考下:爲什麼我們要修改呢?除了原有邏輯有 bug 要修復、代碼重構外,一個重要的原因是需求發生了變化,是變化導致我們要對原有的邏輯進行修改。如果沒有修改的場景,也就沒有所謂的高內聚、低耦合之說了。因此設計原則的底層邏輯就是讓軟件能夠較好地應對變化,降本增效。

如何落地實踐

設計原則只是一個指導的方針,離落地實踐還有很大的一段距離,就像有些同學說設計原則我懂了,但我依然運用不到。實際上這個問題的本質還是對設計原則的底層邏輯沒有理解,沒有洞察出變化關注點,怎麼解決這個問題呢?設計模式給出的答案:找到變化、封裝變化。

設計模式的本質

案例實踐

當調用的接口有不同的實現時(入參、出參、接口都不相同),需要抽象出一層防腐層,怎麼去實現呢?接下來分別看 2 個案例,這 2 個案例的側重點不一樣,一個是偏行爲的抽象,一個是偏結構的抽象。

店鋪品牌查詢

店鋪需要查詢店鋪品牌信息,然而 Lazada 和 AE 的接口是不一樣的,怎麼抽象防腐層呢?

首先最簡單的方案很容易想到,就是定義一個接口,然後有兩個實現。它的優點是層次簡單,大家基本看了就懂。它的缺點也是明顯的,在兩個實現類中,職責不一單一,承擔了兩個職責:一個是實現店鋪品牌的查詢,另一個是數據轉換。

根據方案一提到的缺點,很容易想到使用適配器模式,將之前的類拆成兩個類:一個類是調用對應的品牌服務;另一個類做數據適配轉換。不過此時的方式還有一個缺點就是在國際化場景下,要考慮多租戶之間的隔離,比如 Lazada 有多個站點,如何實現更細粒度的差異呢?方案三基於這些的思考就產生了。

方案三是引入了多租戶框架,能夠支撐多租戶場景。

店鋪優惠券查詢

有一種"萬金油"式開發模式:組裝參數、調用接口、解析響應結果,你會發現這種模式太萬能了,適合所有的場景,這樣的開發模式也即是"事務腳本模式"或者"麪條型代碼"。

優惠券查詢的案例,用領域建模的模式,首先思考有哪些實體。優惠券查詢的本質:通過 xx 條件查詢返回滿足條件的優惠券集合。對於優惠券來講,有兩類信息至關重要。一個是優惠券的規格信息,如優惠券名稱、優惠金額、有效期等;另一個是優惠券的限制條件。在查詢的時候,是查店鋪優惠券,還是查粉絲優惠券,或者是查詢商品優惠券……。因此分開兩部分抽象優惠券:一個是優惠券查詢請求;另一個是優惠券規格實體。

如果按照這樣的設計,有一個缺點是業務方理解複雜度會上升,它是偏底層實現,沒有做到使用簡單。優惠券偏產品交付而非僅僅功能交付。因此在底層實現之上,再抽象出產品組件,這樣業務方使用起來就比較簡單。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章