如何編寫高質量的C#程式碼(一)

溪源More發表於2020-09-07

從”整潔程式碼“談起

一千個讀者,就有一千個哈姆雷特,程式碼質量也同樣如此。

想必每一個對於程式碼有追求的開發者,對於“高質量”這個詞,或多或少都有自己的一絲理解。當我在長沙.NET技術社群群丟擲這個問題時,眾說紛紜。有人說註釋齊全、可讀性高,就是高質量;有人說變數命名、程式碼層次清晰,就說高質量的程式碼;有人說那些使用了新特性的程式碼,很多都是高質量程式碼;也有人說,高質量的程式碼是個偽命題,因為他往往要花大量的精力才能精心打磨,有這個時間,產品早就黃了。

說到”高質量“程式碼,就不得不提”整潔程式碼”。這個概念來源於暢銷書《程式碼整潔之道》(The Clean Code)中,鮑勃大叔引入了這個整潔程式碼的概念。

他認為:

寫整潔程式碼,需要遵循大量的小技巧,貫徹艱苦習得的‘整潔感’”,這種“程式碼感”就說關鍵所在。有些人生而有之。有的人費點勁才能得到。它不僅讓我們看到程式碼的優劣,還予我們以借戒規之力化優為列的攻略。

缺乏”程式碼感”的程式設計師,看混亂是混亂,無處著手,有“程式碼感”的程式設計師,能從混亂中看出其他的可能與變化。“程式碼感”幫助程式設計師選出最好的方案,並指導程式設計師指定修改行動計劃,按圖索驥。

編寫整潔程式碼的程式設計師就像藝術家,他能夠用一系列變化把一塊白板變作由優雅程式碼構成的系統。

這本書值得擺在每一位程式設計師的案頭。許多熱衷於英文原作的讀者都會說國人翻譯的許多作品都失去了原作的韻味,但這本韓磊老師翻譯這本中文版十幾年過去了,印刷了許多版了,也能客觀證明這本譯作的價值。

也許初讀這本書,許多作者提到的手法我們無法短時間內認真體會,但許多讀過這本書都表示,許多想法在我們寫程式碼的時候突然迸濺而出,使得思路能夠更加通達,並達到一種“人碼合一”的狀態。

”程式碼感“

在我們大部分開發者看來,我們開發的程式碼,往往無需涉及過於複雜的業務邏輯或底層技術,只需簡單的使用一些程式碼拼湊,即可按時完成我們的任務,也就說所謂的”CRUD業務開發者“。

但業務系統本身也並非全靠所謂的“無程式碼平臺”或“程式碼生成器”能夠自動開發完成,他依然需要開發者用心去設計其中的邏輯、變數、結構、流程,才能更好的運轉,尤其是要想讓應用系統能夠保持長久的生命力,更需要我們能夠編寫更高質量的程式碼。

在《程式碼整潔之道》中,作者將這種編寫高質量程式碼的能力,稱為“程式碼感”,這種感覺有時需要靈光一現,有時又需要花費大量的精力才能完成。

就像在《灌籃高手》中,安西教練讓大家培養球感:

兩萬個球?寫兩萬個類/方法/程式碼行?確實是一種提高”程式碼感“的好方法。

但跟投球要掌握方法一樣,簡單的重複寫兩萬行程式碼估計很難提高程式碼質量,依然需要大量刻意練習才能帶來質量上的提升。

而如何編寫高質量程式碼,在軟體開發領域,也有一些前人總結出來的良好準則,人們將這些準則,總結為“設計原則”。除了設計原則外,還要許多良好的實踐模式,人們將它們稱為”設計模式“。設計原則就像是內功心法,設計模式,則像招數功夫。

也許我們無法完全遵循這些原則或模式,但能夠靈活的運用,總能給程式碼質量帶來提升。

何為高質量程式碼

我個人認為:高質量程式碼是可讀性強、易於測試,它們能夠恰如其份的表達業務的需要,並能根據業務需要易於修改的程式碼。 高質量的程式碼也許與技術架構、特定API、特定的語言沒有太大關係,但高質量程式碼或許都具備一些相似的特點。

程式碼結構

結構是程式碼的核心,就像高樓的支架,為整個程式碼的完整執行奠定基礎。好的程式碼一定結構清晰,讓人易於理解,並能快速定位問題、解決問題。

有人說好文章的結構特點便是:” 鳳頭、豬肚、豹尾“, 文章的起頭要奇句奪目,引人入勝,如同鳳頭一樣俊美精采;文章的主體要言之有物,緊湊而有氣勢,如同豬肚一樣充實豐滿;文章的結尾要轉出別意,宕開警策,如同豹尾一樣雄勁瀟灑。 程式碼也許無需追求達到這麼高的境界,但遵循一定清晰的程式碼結構也能達到同樣的效果。

結構按照我個人的理解,可能包括以下幾種層面:1、專案資料夾命名;2、分層;3、模組命名;4、程式碼格式。

1、專案資料夾

對於複雜專案,開啟資料夾和解決方案的第一眼,是清晰還是紊亂,往往就是我們對於專案的第一印象。許多資深研發工程師,都會傾向於用數字來對資料夾進行編號,例如對於複雜專案,我們使用如下命名方式對定義解決方案資料夾,雖然不會花特別多的功夫,但會給開發過程帶來許多便利。

當然,由於在Visual Studio中,專案資料夾本身屬於sln解決方案檔案中定義的層級結構,並不會在資源管理器資料夾中體現,所以有時還需要在資源管理器資料夾中也定義類似的層級結構。

01 基礎服務
02 框架服務
03 應用服務
   01 工作流服務
   02 許可權服務
   03 日誌服務

2、分層

分層式架構大家都習以為常,其中尤其以三層架構(使用者表現層,業務邏輯層,資料訪問層)已經深入人心,成為許多.NET開發者的普遍認可,而領域驅動設計最常見的則是四層式領域驅動設計(使用者介面層,應用層,領域層,基礎設施層)。

分層式架構體現了”關注度分離“的原則,在進行軟體開發過程中,可以根據需求,找到對應的邏輯分層,進行程式碼實現;有時不同邏輯分層的元件會以各自不同的發展速度迭代以滿足不同的需求;在適當的情況下,還能採用分散式架構,讓不同層執行在不同的基礎設施中,期間通過rpc等方式保持通訊,給架構留下了足夠的彈性空間。

設計分層式架構並非越多越好,儘量控制在三到四層就足夠了,不然會陷入”千層餅“的陷阱,過多的分層和過少的分層,其實沒有任何區別。

對於後端工程師來說,理解分層式架構並不困難,難的是要識別哪裡邏輯程式碼應該歸屬於哪一層;而許多對於方興未艾的前端技術來說,如何分層,卻似乎並不是一件容易的事,由於前端業務要適應來自使用者層面的無窮變化,很容易就陷入“義大利麵”式的程式碼混亂中。vuex框架為前端開發者提供了一種良好的示例,有時無需深入瞭解vuex的機制,只需"模仿"這種分層方法,就能寫出更加易於維護的前端程式碼了。

3、模組(類庫)

模組的設計和耦合性

在.NET開發中,模組有時是一個獨立的專案,並以一個獨立dll(類庫)的形式進行分發。模組也是最為常見的一種程式碼實踐,但在《領域驅動設計·軟體核心複雜性應對之道》一書中,作者埃裡克·埃文斯卻指出模組的運用,引起了“認知過載”的問題:

認知負荷理論認為,在問題解決和學習過程中的各種認知加工活動均需要消耗認知資源,若所有活動所需的資源總量超過個體擁有的資源總量,就會引起資源的分配不足,從而影響個體學習或問題解決的效率,這種問題就說“認知過載”。

這段理論確實有點拗口,對應到軟體開發過程中,用通俗的說法,就是這個包承載的知識量太大了,把原本可以分離到多個模組中的邏輯程式碼都囊括進來,使得其反而降低了開發的效率。

尤其是類庫的定義,不同的開發者有不同的習慣,有時按技術來劃分,有時又按業務場景來劃分,有時分拆,有時組合,“千人千面”,不連貫的設計思想,和“能用就行”的想法混合在一起,很容易就造成了一鍋粥的情況。

在.NET專案中,每用一個using,就引入了一種耦合,而使用了new方法,建立了一個物件的示例,又引入了一個物件的耦合。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using xxx.Core;
using xxx.Infrastructure.Extension;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;

而設計優良的程式碼模組,則可以讓依賴儘可能的減少。

模組其實也是實踐“高內聚,低耦合”思想的主要陣地,如果業務相關性很高的物件被劃分到不同的模組中,往往會使得開發者很難理解它們在業務上的作用,也會導致模組間的耦合進一步提高。

因此,好的模組設計應該將那些具有緊密概念關係的模型元素集中在一起,並能描述該模型元素的職能,使之成為一個內聚的概念集合。

元件設計的原則

關於如何設計模組,在《敏捷軟體開發 原則、模式與實踐》一書中,作者引述了以下設計原則基於粒度這個角度為元件的內聚性進行描述:

重用-釋出等價原則(REP)

重用的粒度就是釋出的粒度。REP指出,一個元件的重用粒度可以和釋出粒度一樣大。我們所重用的任何東西都必須被髮布和跟蹤。簡單的編寫一個類,然後聲稱它是可重用的做法是不現實的。只有在建立一個跟蹤系統,為潛在的使用者提供所需要的變更通知、安全性以及支援後,重用才有可能。

共同重用原則(CRP)

一個元件中的所有類應該是共同重用的,如果重用了元件中的一個類,那麼就要重用元件中的所有類。

共同封閉原則(CCP)

元件中的所有類對於同一種性質的變化應該是共同封閉的。一個變化若是對一個封閉的元件產生影響,則將對元件中所有的類產生影響,而對其他元件則不造成任何影響。

從穩定性的角度為元件的內聚性進行描述:

無環依賴原則:

在元件中的依賴關係圖中,不允許存在環。

穩定抽象原則

朝著穩定的方向進行依賴。

設計不能是完全靜態的。要使設計可維護,某種程度的易變性是必要的。我們通過遵循共同封閉原則來達到這個目標。使用這個原則,可以建立對某些變化型別敏感的元件。這些元件設計為可變的。我們期望他們變化。

穩定抽象原則

元件的抽象程度應該與其穩定程度一致。

4、程式碼格式

類的基本結構

程式碼格式,就是一個C#程式碼檔案的邏輯結構。寫程式碼其實是一件成本很低的事,但維護程式碼,卻是一件成本很高的事。開發一個功能,只需短短几十分鐘時間,但如果我們要去找出程式碼中存在的缺陷,卻往往需要花費大量的時間。

這就客觀上要求,我們書寫的程式碼應該儘量方便閱讀(可讀性)、檢索(快速找問題)、易於維護,而書寫出“格式化”的程式碼,大概是我們能夠提高程式碼質量的第一步。

對於書寫的程式碼,大部分都是從上往下閱讀,在需要閱讀的程式碼較多量時,往往會選擇摺疊到定義,這樣就能一眼看出每個方法的用途,要達到這個效果,就意味著我們需要精心設計安排程式碼的垂直格式。有經驗的開發者往往會按照這種結構。

私有欄位:定義類內部的基本成員,高層次概念,常量,和引入的演算法。

建構函式:定義類的建立過程。

公共方法:定義類為外部暴露的行為。

私有方法:定義類為內部提供的行為。

類的格式要求

在《程式碼整潔之道》這本書中,作者介紹了他對於程式碼的格式要求:

垂直格式

程式碼檔案的長度控制在200-500行左右,且短檔案通常比長檔案易於理解。垂直閱讀時,頂部是粗線條概述,隱藏了故事細節,然後再不斷展開。

每行展示一個表示式或一個子句,尤其是C#的鏈式語法,儘量一行程式碼就是一個方法。

entity.Property(e => e.Memo)
.HasMaxLength(500)
.IsUnicode(false)
.HasComment("備註");

每組程式碼行展示一個完整的思路,思路間用空白行隔開。垂直方向上,靠近的程式碼可以展示它們之間的緊密關係,能夠讓程式碼更好閱讀。

變數宣告應儘可能靠近其使用位置,因為函式很短,本地變數應該在函式的頂部出現。一個函式呼叫了另外一個函式,應該把它們放到一起,且呼叫者應該在被呼叫者上面。概念相關的程式碼應該放到一起,相關性越強,彼此之間的距離就該越短。

橫向格式

橫向首先表現在程式碼的寬度上,儘量控制在一行程式碼不超過120個字元。

水平方向上,可以用空格字元把彼此緊密相關的變數或物件連線在一起,也可以用空格將相關性較弱的物件分割開。

注意水平縮排和左對齊,尤其是上面提到的鏈式語法,如果點號沒對齊,簡直讓人難受。

entity.Property(e => e.UserId)
   .HasMaxLength(10)
  .IsUnicode(false)
.IsFixedLength();
小結

本文對如何編寫高質量程式碼進行了一些簡單的概述,介紹了程式碼的分層、元件(包)的設計、以及整潔程式碼中的一些開發實踐,通過了解這些知識,能夠讓我們逐漸形成自己對於程式碼的體會,並通過不斷的練習,將能夠提高我們的程式碼能力。

當然,有時,寫文件、適當的做一些軟體工程設計,看起來與完成程式碼編寫無關,但也同樣是提高程式碼質量的一種手法,通過就像許多好文章往往會先搭框架,好程式碼也同樣如此。

根據業務需要,畫一波流程圖、領域模型圖、類圖、時序圖能夠讓我們的思路提前沉澱,讓我們的開發過程更加流暢,更能開發出高質量的程式碼。

下一篇,將對規範命名、註釋、設計向量、設計原則、設計模式進行一些討論。

相關文章