天天看點

如何寫出一手好代碼(上篇 - 理論儲備)?

作者:慕楓技術筆記

#頭條創作挑戰賽#

無論是剛入行的新手還是已經工作多年的老司機,都希望自己可以寫一手好代碼,這樣在代碼 CR 的時候就可以悄悄驚豔所有人。特别是對于剛入職的新同學來說,代碼寫得好可以幫助自己在新環境快速建立技術影響力。因為對于從事 IT 網際網路研發工作的同學來說,技術能力是研發同學的立身之本,而寫代碼的能力又是技術能力的重要展現。但可惜的是理想很豐滿,現實很骨感。結合慕楓自己的經驗來看,我們在工作中其實沒那麼容易可以看到寫得很好的代碼。造成這種情況的原因也許很多,但是無論什麼原因都不應該妨礙我們對于寫好代碼的追求。今天慕楓就和大家探讨下到底怎樣做才能寫出一手大家都認為好的代碼?

哪些因素制約好代碼的産生?

我們首先來分析下到底哪些因素造成了現實工作中好代碼難以産出。因為隻有搞清楚了這個問題才能對症下藥,這樣在我們自己寫代碼的時候才能盡量避免這些問題影響我們寫好代碼。

假如讓我們說出哪些是爛代碼,我們也許會羅列出來代碼不易了解、沒有注釋、方法或者類詞不達意、分層不合理、不夠抽象、單個方法過長、單個類過長、代碼難以維護每次改動都牽一發動全身、重複代碼過多等等,這些都是我們在實際項目開發過長中經常遇到的代碼問題。那麼到底是什麼原因造成了現實項目中有這麼多的代碼問題呢?慕楓認為主要存在以下三方面的原因。

如何寫出一手好代碼(上篇 - 理論儲備)?

1、項目倒排時間不夠

項目需求倒排導緻沒有時間在寫代碼前好好進行設計,是以隻能先快速滿足需求等後面有時間再優化(大機率是沒有時間的)。這就造成技術同學在寫代碼的時候怎麼快怎麼寫,優先把功能實作了再說,很多該考慮的細節就不會考慮那麼多,該處理的異常沒有進行處理,是以可能寫出來的代碼可以說是一次性代碼,隻針對目前的業務場景,基本沒什麼擴充性可言。

2、團隊技術氛圍不足

團隊内技術氛圍不是很濃厚,本來你是想好好把代碼寫好的,但是發現大家都在短平快的寫代碼,而且沒有太多人關心代碼寫的好不好,隻關心需求有沒有按時完成。在這樣的團隊氛圍影響之下,自己寫出來的代碼也在慢慢地妥協。像在阿裡這樣的一線網際網路公司,團隊中的代碼文化還是很強的,很多技術團隊在需求上線前必須要進行代碼 CR,CR 不過的代碼不允許上線。是以好的團隊技術氛圍會促使你不得不把代碼寫好,否則在代碼 CR 的時候就等着接受暴風雨般的吐槽吧。

3、自身技術水準有限

第三個原因就是自身的技術水準有限,設計模式不知道該在什麼樣的業務場景下使用,架構的進階用法沒有掌握,經驗不足導緻異常情況經常考慮不到。自己本身沒有把代碼寫好的追求,總想着能滿足需求代碼能跑就行。

以上大概是我們實際工作中導緻我們不能産出好代碼最主要的三大原因,第一個原因我們基本無法改變,因為在網際網路行業競争本身就非常激烈,誰能先推出新業務優化使用者體驗,誰就能占得市場先機。是以項目倒排必定是常有的事情,也是無法避免的事情。第二個原因,如果你自己是團隊的 TL,那麼盡量在團隊中去營造代碼 CR 的文化,提升團隊中的技術氛圍。因為代碼是技術團隊的根本,所有的業務效果落地都需要通過代碼來實作,是以好的代碼可以幫助團隊減少 Bug 出現的機率、提升大家的代碼效率進而達到降低人力物力成本的目的。如果你不是團隊的 TL,同時團隊中的技術氛圍也沒那麼足,那麼我們也不要放棄治療,先把自己負責的子產品的代碼寫好,一點點影響團隊,逐漸喚起大家對于好代碼的重視。

前兩個因素都屬于環境因素,也許我們不好改變,但是對于第三個因素,我覺得我們可以通過理論知識的學習,不斷的代碼實踐以及思考總結是可以改變的,是以本文主要還是讨論如何通過改變自己來把代碼寫好。

到底什麼是好代碼?

要想寫出好的代碼,首先我們得知道什麼樣的代碼才是好代碼。但是好這個字本身就具有較強的主觀性,正所謂一千個讀者心中就有一千個哈姆雷特。是以我們需要先統一一下好代碼的标準,有了标準之後我們再來探讨到底怎麼做才能寫出好代碼。

我相信大家肯定聽說過代碼可讀性、代碼擴充性、可維護性等詞彙來描述好代碼的特點,實際上這些形容詞都是從不同方面對代碼進行了闡述。但是在慕楓看來,在實際的項目開發中,可維護性以及高魯棒性是好代碼的兩個比較核心的衡量标準。因為無論是開發新需求還是修複 Bug,都是在原有的平台代碼中進行修改,如果原來代碼的擴充性比較強,那麼我們編碼的時候就就可以做到最小化修改,降低引入問題的風險。而魯棒性高的代碼線上上出現 Bug 的機率相對來說就第一點,對于維護線上服務的穩定性具有重要意義。

可維護性

我們都知道代碼開發并不是一個人的工作,通常涉及到很多人團隊合作。是以慕楓認為代碼的可維護性是好代碼的第一要義。而可維護性主要展現在代碼可讀容易了解以及修改友善容易擴充這兩方面,下面分别進行闡述說明。

代碼可讀

我們寫出來的代碼不僅僅要自己能看得懂自己寫的代碼,别人也應該可以輕松看得懂你的代碼。在一線的網際網路大廠中工作内容發生變化是常有的事情,如果别人接手我們的代碼或者我們接手别人的代碼時,可讀性強的代碼無疑可以減少大家了解業務的時間成本。因為代碼是最直接的業務表現,那些所謂的設計文檔要麼過時要麼寫的非常粗略,基本不太能指導我們熟悉業務。那麼什麼樣的代碼稱得上可讀性強呢?

命名準确

無論是包的命名、類的命名、方法的命名還是變量的命名都能很準确地表達業務含義,讓人可以看其名知其義。命名應該和實際的代碼邏輯相比對,否則不合适的命名隻會讓人丈二和尚摸不着腦袋誤導看代碼的同學。以前看代碼的時候我看過以 main 作為類中的方法名稱,是以得看完這個方法的實作邏輯才能明白它到底幹什麼的,這對于後期維護的同學來說非常不友好。

代碼注釋

另外就是必要的注釋,有些同學非常自信覺得自己寫的代碼很好懂,根本不需要寫什麼注釋。結果自己過了一兩個月再回頭看自己的代碼的時候,死活想不起來某段代碼為什麼要這麼寫。當然我們不必每一行代碼都寫注釋,但是該注釋的地方就要寫注釋,特别是一些邏輯比較複雜,業務性比較強的地方,既友善自己以後排查問題也友善後面維護的同學了解業務。是以不要對自己寫的代碼過于自信,間隔時間一長也許連你自己都未必記得代碼為什麼這麼寫。

結構清晰

無論是服務的包結構還是代碼結構都展現了技術同學對于技術的了解,是以即便是不深入看代碼邏輯,通過包結構的劃分、子產品的劃分類結構的設計已經基本可以判斷出來項目的代碼品質了。我們在進行包結構設計的時候可以遵循依賴倒置的原則,讓非核心層依賴核心層。

如何寫出一手好代碼(上篇 - 理論儲備)?

可擴充性

随着業務需求的不斷變化,技術同學免不了在原有的代碼邏輯中進行修改。是以項目代碼的可擴充性直接影響着後期維護的成本。如果改一個小需求就需要對原有的代碼大動幹戈,修改的地方越多引入 Bug 的風險就會越大。我們都知道線上的故障有七八成都是由于變更引起的,是以可擴充性強的代碼可以有效控制變更的範圍。

高魯棒性

當我們說到代碼魯棒性高的時候,實際就是說代碼比較健壯,能夠應對各種輸入,即便出現異常也會有對應的異常處理機制進行響應而不至于直接崩潰。而項目開發不是一個人的工作,通常都是團隊合作,是以我們寫的代碼無時無刻不在和别人的代碼進行互動,是以我們負責的代碼子產品總是在處理可能正常可能異常的輸入。如果不能對可能出現的異常輸入進行妥善的防禦性處理,那麼可能就會造成 Bug 的産生,嚴重情況下甚至會影響系統正常運作。是以好的代碼除了友善擴充友善維護之外,它必定也是高魯棒性的,否則如果每天 Bug 滿天飛,哪有時間和精力去琢磨代碼的可擴充性,大部分精力都用來修複 Bug,長此以往自己也會感覺身心俱疲,總是感覺自己沒什麼成長。

如何寫出好代碼?

強烈内在驅動

為什麼我把強烈的内在驅動擺在首要位置,主要是因為我覺得程式員隻有有了想把代碼寫好的願望,才能真正驅動自己寫出來好代碼。否則即便掌握了各種設計原則以及優化技巧,但是自己沒有寫好代碼的内在驅動,總是覺得程式又不是不能用,或者覺得代碼和自己有一個能跑就行,亦或是抱着後面有時間再優化的态度(基本是沒時間)是不可能寫好代碼的。是以首先我們得有寫好代碼的内在驅動和願望,我們才能有把代碼寫好的可能。不過話又說回來,内在驅動是基礎,全是感情沒有技巧肯定也不行。

沉澱業務模型

談完了内在驅動這個感情,我們就要來看看要掌握哪些技巧才能幫助我們寫出來好代碼,首當其沖的就是業務領域模型,因為它是領域業務在工程代碼中的落地也是整個服務的核心,不過遺憾的是很多同學并沒有意識到它的重要性,甚至經常會把資料模型和業務模型相混淆。而我自己在在團隊中落地 DDD 領域驅動設計的時候,被技術同學問過比較多的問題就是資料庫表對應的資料實體滿足不了業務需要嗎?為什麼還需要業務領域模型?那麼想要回答這些問題,我們得先搞清楚到底什麼是領域模型,它到底能給技術團隊帶來什麼。

從本質上來說領域模型就是我們對于本行業業務領域的認知,展現了你對行業認知的沉澱以及外化表現。那麼怎麼展現你對行業領域業務認知的深度呢?領域模型就是很好的驗證手段,對行業認知越深刻的同學建構的領域模型越能夠刻畫現實中的業務場景,我們也可以認為領域模型是現實世界業務場景到代碼世界的映射,同時它也是公司重要的業務資産。那麼每個行業的業務認知又是從哪裡來的呢?實際上就從實際的業務場景中抽象出來的。是以領域模型的建立通常都是伴随着業務需求的出現。是以領域模型是核心,包含了業務概念以及概念之間的關系,它可以幫助團隊統一認識以及指導設計。

如何寫出一手好代碼(上篇 - 理論儲備)?

但是領域模組化具有一定的門檻,其中包含了很多難以了解的概念,這也造成了在很多技術團隊中難以落地。但是在阿裡等國内一線網際網路公司卻有着廣泛的應用,因為 DDD 領域驅動設計可以指導我們應對複雜系統的設計開發,控制系統複雜度,幫助我們劃分業務域,将業務模型域實作細節相分離。是以慕楓覺得讓大家認識到 DDD 領域驅動設計以及領域模型的的重要性比如何玩轉 DDD 本身更加重要。

如何寫出一手好代碼(上篇 - 理論儲備)?

另外在這裡不得不提一下資料模型和領域模型的差別,在實際的工作中我發現很多同學都容易将這兩者混淆。領域模型關注的是業務場景下的領域知識,是業務需求中概念以及概念之間的關系,它的存在就是顯示的精确的表達業務語義。而資料模型關注的是業務資料如何存儲,如何擴充以及如何操作性能更高。是以他們關注的層面不同,領域模型關注業務,資料模型關心實作。

這裡可以舉個例子給大家說明一下,假設有這樣的業務場景,告警規則中存在一個規則範圍的概念,主要可以給出不同的告警取值判斷的範圍,比如某個接口調用次數失敗的最大值,或者裝置線上數量不能低于某個最小值等等,是以有了如下簡化版本的領域模型。

如何寫出一手好代碼(上篇 - 理論儲備)?

那麼在實際實作落地的時候,就很自然想到将 AlarmRule 以及 RuleRange 分别用一個表進行進行存儲。這其實就是把領域模型和資料模型混淆的典型例子,實際上我們沒有必要搞兩張表來存儲,一張表其實就夠了,主要有以下兩個原因:

1、寫代碼的時候我們維護一張表肯定比維護兩張表操作起來更加友善;

2、另外萬一後面 ruleRange 有新的變化,增減了新的判斷條件,我們還得要修改 rule_ranged 字段,不利于後期的擴充。

如何寫出一手好代碼(上篇 - 理論儲備)?

是以我們用一張表來就進行存儲就好了,多一個 json 類型的字段,專門存儲門檻值判斷範圍。隻不過在領域模型中我們需要把 c_rule_range 定義為一個對象,這樣在代碼層面操作起來比較友善。

如何寫出一手好代碼(上篇 - 理論儲備)?

牢記設計原則

無論設計原則還是設計模式,都是先驅們在以往大量軟體設計開發實踐中總結出來的寶貴經驗,是以我們在項目開發中完全可以站在巨人的肩膀上利用這些設計原則指導我們進行編碼。當然如果我們想熟練使用這些設計原則,就必須先要了解他們,搞清楚這些設計原則到底是為了解決什麼問題而産生的。

我們不妨仔細想一想,平日時間裡技術同學的開發工作基本上都是在已有的服務中進行新需求開發或者在原有的邏輯中修修改改。是以如果因為一個需求需要修改原有代碼邏輯,我們總是希望修改的地方越少越好,否則如果修改的地方多了,那麼引入的 Bug 風險就會越大。即便是項目需要進行重構的情況,那我們也希望重構後的服務或者元件可以滿足高内聚低耦合的大要求,這樣在未來進行需求開發的時候可以更加友善的進行修改。這也是我們希望我們開發的代碼高内聚低耦合的原因。可以看得出來,設計原則的核心思想就是幫助技術人員開發的軟體平台能夠更好地應對各種各樣的需求變化,進而最終達到降低維護成本,提高工作效率的目的。

當我們說到設計原則的時候,通常都會想到 SOLID 五大原則,這裡所說的設計原則主要包括 SOLID 原則、迪米特法則。

單一職責原則

對于一個方法、類或者子產品來說,它的職責應該是單一的,方法、類或者子產品應該隻負責處理一個業務。這個原則應該很好了解,當我們在寫代碼的時候,無論是方法、類以及子產品都應該從功能或者業務的角度考慮将無關的邏輯抽離出去。為什麼這麼做呢?主要還是為了能夠實作代碼業務功能的原子化操作,這樣即便未來進行修改的時候影響的範圍也會變得有限。如果我們不遵守單一職責原則,那麼在修改代碼邏輯的時候很可能影響了其他業務的邏輯,造成修改影響範圍不可控的情況。

You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

不過需要說明的是,這裡的所說的單一職責是針對目前的業務場景來說的,也許随着業務的發展和場景的擴充,原來滿足單一職責的方法、類或者子產品可能現在就不滿足了需要進一步的拆分細化。

開閉原則

慕楓認為開閉原則與其說它是一種設計原則,不如說它是一種軟體設計指導思想。無論我們編寫架構代碼還是業務代碼都可以在開閉原則這樣的核心思想指導下進行設計。

Software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。

所謂開閉原則指的就是我們開發的架構、子產品以及類等軟體實體應該對擴充開放,對修改關閉。這個原則看上去很容易了解,但是在進行項目實際落地的時候卻不是一件容易的事情。因為對于擴充以及修改并沒有明确的定義,到底什麼樣的代碼才是擴充,什麼樣的代碼才是修改?這些問題不搞清楚的話,我們很難把開閉原則落地到實際的項目開發中。

結合自己的開發經驗可以這麼了解,假設我們在項目中開發一個功能的時候,如果能做到不修改已有代碼邏輯,而是在原有代碼結構中擴充新的子產品、類或者方法的話,那麼我們認為代碼是䄦開閉原則的。當然這也不是絕對的,比如假設你修改一個原有邏輯中的判斷條件的門檻值,那隻能在原有代碼邏輯中進行修改。總不能因為要滿足這個原則非要搞出來。是以我覺得我們不必要教條的去追求滿足開閉原則,而是從大方向上以及整體上考慮滿足開閉原則。

裡氏替換原則

在面向對象思想建構的程式中,子類對象可以替換程式中任何地方出現的父類對象,同時還能保證程式的邏輯不變以及正确性不變,這就是裡氏替換原則的字面了解。不知道大家有沒有發現,這個裡氏替換原則看上去和 Java 中的多态一樣一樣的。實際上他們還是有差別的,多态是面向對象程式設計的特性,是重要的代碼實作思路。而裡氏替換原則是一種設計原則,約定子類不能破壞父類定義好的邏輯以及異常處理。

比如在倉儲業務域中,父類中有對揀貨任務進行排序的 sortPickingTaskByTime()方法,它是按照任務建立的時間對到來的揀貨任務進行排序,那麼我們在子類實作的時候如果在 sortPickingTaskByTime()方法内部按照揀貨任務涉及的商品品類進行排序,那麼明顯是不符合裡氏替換原則的,但是從多态的角度來說或者從文法的角度來說卻沒有問題。

裡氏替換原則的核心思想就是按照約定辦事,父類約定好了的行為,子類實作需要嚴格遵守。那麼裡氏替換原則對于實際編碼有什麼指導意義呢?比如上文所說的 sortPickingTaskByTime()排序方法,如果父類中的算法實作效率不高,我們可以在子類中進行優化,有了裡氏替換原則就可以通過子類改進目前已有的實作。另外父類中的方法定義就是契約,可以指導我們後面的編碼。

接口隔離原則

所謂接口隔離說的是接口調用方不應該被迫依賴它不需要的接口。怎麼了解這句話呢?按照慕楓自己的了解,接口調用方隻關心和自己業務相關的接口,其他不相關的接口應該隔離到其他接口中。

Clients should not be forced to depend upon interfaces that they do not use。

從擴充能力層面來看,我們定義接口的時候按照原子能力進行定義,避免了定義一個大而全的接口,這樣在進行擴充的時候就可以按照具體的原子能力來進行,這樣無論是靈活性還是通用性上面都會更加滿足需求。

從實作上來說,如果實作方僅僅需要實作它以來的接口功能就好,它不需要的接口功能就不需要實作,這樣也會大大降低代碼實作量。當我們擴充或者修改代碼的時候能夠做到最小化的修改。

依賴倒置原則 依賴倒置原則不太容易了解,但是我們在實際的項目開發中卻每一天都在使用,隻是我們可能沒太在意罷了。

High-level modules shouldn't depend on low-level modules. Both modules shoud depend on abstractions.In addition,abstractions shouldn't depend on details.Details depend on abstractions.

按照字面意思了解,高層級子產品不應該依賴低層級子產品,同時兩者都應該依賴于抽象。另外抽象不應該依賴于細節,細節應該依賴于抽象。用大白話來說主要是兩個核心點,一是面向接口程式設計,另一個是基礎層依賴核心層。

面向接口程式設計這個應該很好了解,因為接口定義了清晰的協定規範,研發同學可以基于接口進行開發。

如何寫出一手好代碼(上篇 - 理論儲備)?

迪米特法則

迪米特法則看名字是一點不知道它是幹什麼的,簡單來說就是類和類之間能不要有關系就不要有關系,實在沒辦法必須要有關系的那也盡量隻依賴必要的接口。這樣說起來感覺還是比較抽象。看下面的圖就明白了,左邊的各個子產品拆分比較獨立,符合單一職責原則,同時子產品間隻依賴它所需要的子產品,而下圖右邊的子產品拆分不夠獨立,A 子產品本來隻需要依賴 F 子產品,但是 FG 子產品顆粒度較大,導緻不得不依賴 G 子產品的接口,顯然這是不符合迪米特法則的。

如何寫出一手好代碼(上篇 - 理論儲備)?

當我們有了寫出來的代碼能夠實作高内聚低耦合、易擴充以及易維護願景之後,那就要好好學習一些代碼實作的設計原則,這些設計原則在戰略層面可以指導我們擴充性強的代碼應該往哪些方向進行設計考慮。而有了指導思想之後,結合不同場景下的設計模式就自然催生出來我們想要的結果。

如何寫出一手好代碼(上篇 - 理論儲備)?

運用設計模式

設計模式是先驅們在實踐的基礎上總結出來可以落地的代碼實作模闆,針對一些業務場景提供代碼級解決方案。我們根據各個設計模式的能力特點可以将 23 種設計模式分類為建立型模式、結構型模式以及行為型模式。這裡不再對設計模式進行展開說明,後面有時間可以寫系列文章專門進行介紹。不過我們需要清楚的是這 23 種設計模式就是程式員寫代碼打天下的招式,而提升代碼擴充性才是最終目的。

如何寫出一手好代碼(上篇 - 理論儲備)?

面向失敗編碼

代碼中的異常處理往往最能展現技術同學的編碼功力。完成一個需求并不難,但是能夠考慮到各種異常情況,在異常發生的時候依然可以得到預想輸出的代碼,卻不是每個程式員都能寫出來的。 是以無論是寫代碼還是系統設計,都要有面向失敗進行設計的意識,每一個業務流程都要考慮如果失敗了應該怎麼辦,盡可能考慮周全可能會出現的意外情況,同時針對這些意外情況設計相應的兜底措施,以實作防禦性編碼。

這裡假設有這樣的業務場景,當我們的業務中有調用外部服務接口的邏輯,那麼我們在編寫這部分代碼的時候就需要考慮面向失敗進行編碼。因為調用外部接口有可能成功,有可能失敗。如果接口調用成功自然沒什麼好說的,繼續執行後續的業務邏輯就好。但是如果調用失敗了怎麼辦,是直接将調用異常傳回還是進行重試,如果重試還是失敗應該怎麼辦,需不需要設計下重試的政策,比如連續重試三次都失敗的話,後續間隔固定時間再進行重試等等。當然我們并不需要在每個這樣的業務流程中這麼做,在一些比較核心的業務鍊路中不能出錯的流程中要有兜底措施。

如何寫出一手好代碼(上篇 - 理論儲備)?

總結

本文主要從理論層面為大家介紹寫好代碼的需要哪些知識儲備,下一篇會從具體業務場景出發,具體實操怎麼結合這些理論知識來把代碼寫好。不過我們必須認識到好代碼是需要不斷打磨的,并非一朝一夕就能練就,總是需要在不斷的實踐,不斷的思考,不斷的體會以及不斷的沉澱中實作代碼能力的提升。左手設計原則,右手設計模式,心中領域模型再加上強烈的内在驅動,我相信我們有信心一定可以寫出一手好代碼。