計算機程序設計:7大編程原則

寫出乾淨優雅的代碼並不是一件容易的事情。

編程的工作同石匠的工作相類似,即是技術活,也是體力活,而編寫優秀的軟件,算是一件比較難的事。編程大牛們並不是直接上手編寫,而是根據需求進行設計,不但將代碼中 Bug 出現的機率降到最低,還要讓代碼具有高可讀性,高安全性等等。

這些設計原理源於對實際軟件開發現場的分析,是提高代碼質量的經驗結晶。

01
簡單性原則
Simplicity Principle
What:追求簡單
簡單性原則就是追求簡單。

說得極端一點,就是自始至終都以最簡單的邏輯編寫代碼,讓編程初學者一眼就能看懂。

因此,在編程時我們要重視的是局部的完整性,而不是複雜的整體關聯性。

Why:Bug 喜歡出現在複雜的地方
軟件故障常集中在某一個區域,而這些區域都有一個共同的特點,那就是複雜。編寫代碼時如果追求簡單易懂,代碼就很難出現問題。
不過,簡單易懂的代碼往往給人一種不夠專業的感覺。這也是經驗老到的程序員喜歡寫老練高深的代碼的原因。所以我們要有足夠的定力來抵擋這種誘惑。
Do:編寫自然的代碼
努力寫出自然的代碼。放下高超的技巧,堅持用簡單的邏輯編寫代碼。

既然故障集中在代碼複雜的區域,那我們只要讓代碼簡單到讓故障無處可藏即可。不要盲目地讓代碼複雜化、臃腫化,要保證代碼簡潔。

02
同構原則
Isomorphism Principle
What:力求規範
同構原則就是力求規範。

同等對待相同的東西,堅持不搞特殊。同等對待,舉例來說就 是同一個模塊管理的數值全部採用同一單位、公有函數的參數個數統一等。

Why:不同的東西會更顯眼
相同的東西用相同的形式表現能夠使不同的東西更加突出。不同的 東西往往容易產生 bug。遵循同構原則能讓我們更容易嗅出代碼的異樣, 從而找出問題所在。

圖表和工業製品在設計上追求平衡之美,在這一點上,同構原則也 有著相似之處。統一的代碼頗具美感,而美的東西一般更容易讓人接 受,因此統一的代碼有較高的可讀性。

Do:編寫符合規範的代碼
我們要讓代碼符合一定的規範。不過,這會與程序員的自我表現欲相衝突。
為了展現自己的實力,有些程序員會無視編程規範,編寫獨特的代碼。可靠與簡單是代碼不可或缺的性質,但這些程序員常常在無意間讓代碼變得複雜。
這就把智慧與個性用錯了地方。小小的自我滿足遠不及代碼質量重要。所以在編寫代碼時,務必克制住自己的表現欲,以規範為先。
03
對稱原則
Symmetry Principle
What:講究形式上的對稱
講究形式上的對稱。

對稱原則就是講究形式上的對稱,比如有上就有下,有左就有右, 有主動就有被動。

也就是說,我們在思考一個處理時,也要想到與之成對的處理。比 如有給標誌位置 1 的處理,就要有給標誌位置 0 的處理。

Why:幫助讀代碼的人推測後面的代碼
具有對稱性的代碼能夠幫助讀代碼的人推測後面的代碼,提高其理解代碼的速度。同時,對稱性會給代碼帶來美感,這同樣有助於他人理解代碼。

此外,設計代碼時將對稱性納入考慮的範圍能防止我們在思考問題時出現遺漏。如果說代碼的條件分支是故障的溫床,那麼對稱性就是思考的框架,能有效阻止條件遺漏。

Do:編寫有對稱性的代碼
在出現「條件」的時候,我們要注意它的「反條件」。每個控制條件都存在與之成對的反條件(與指示條件相反的條件)。要注意條件與反條件的統一,保證控制條件具有統一性。
我們還要考慮到例外情況並極力避免其發生。例外情況的特殊性會破壞對稱性,成為故障的溫床。特殊情況過多意味著需求沒有得到整理。此時應重新審視需求,盡量從代碼中剔除例外情況。
命名也要講究對稱性。命名時建議使用 set/get、start/stop、begin/ end 和 push/pop 等成對的詞語。
04
層次原則
Hierarchy Principle
What:講究層次
注意事物的主從關係、前後關係和本末關係等層次關係,整理事物的關聯性。
不同層次各司其職,同種處理不跨越多個層次,這一點非常重要。比如執行了獲取資源的處理,那麼釋放資源的處理就要在相同的層次進行。又比如互斥控制的標誌位置 1 和置 0 的處理要在同一層次進行。
Why:層次結構有助於提高代碼的可讀性
有明確層次結構的代碼能幫助讀代碼的人抽象理解代碼的整體結構。讀代碼的人可以根據自身需要閱讀下一層次的代碼,掌握更加詳細的信息。

這樣一來就可以提高代碼的可讀性,幫助程序員表達編碼意圖,降低 bug 發生的概率。

Do:編寫有抽象層次結構的代碼
在編寫代碼時設計各部分的抽象程度,構建層次結構。保證同一個層次中的所有代碼抽象程度相同。另外,高層次的代碼要通過外部視角描述低層次的代碼。這樣做能讓調用低層次代碼的高層次代碼更加簡單易懂。
05
線性原則
Linearity Principle
What:處理流程盡量走直線
線性原則就是讓處理流程盡量走直線。
一個功能如果可以通過多個功能的線性結合來實現,那它的結構就會非常簡單。
反過來,用條件分支控制代碼、毫無章法地增加狀態數等行為會讓代碼變得難以理解。我們要避免做出這些行為,提高代碼的可讀性。
Why:直線處理可提高代碼的可讀性
複雜的處理流程是故障的溫床。

故障多出現在複雜的條件語句和循環語句中。另外,goto 等讓流程出現跳躍的語句也是故障的多發地。

如果能讓處理由高層次流向低層次,一氣呵成,代碼的可讀性就會大幅提高。與此同時,可維護性也將提高,添加功能等改良工作將變得更加容易。

一般來說,自上而下的處理流程簡單明快,易於理解。我們應避開複雜反復的處理流程。

Do:盡量不在代碼中使用條件分支
盡量減少條件分支的數量,編寫能讓代碼閱讀者線性地看完整個處理流程的代碼。

為此,我們需要把一些特殊的處理拿到主處理之外。保證處理的統一性,注意處理的流程。記得時不時俯瞰代碼整體,檢查代碼是否存在過於複雜的部分。

另外,對於經過長期維護而變得過於複雜的部分,我們可以考慮對其進行重構。明確且可靠的設計不僅對我們自身有益,還可以給負責維護的人帶來方便。

06
清晰原則
Clarity Principle
What:注意邏輯的清晰性
清晰原則就是注意邏輯的清晰性。

邏輯具有清晰性就代表邏輯能清楚證明自身的正確性。也就是說,我們編寫的代碼要讓人一眼就能判斷出沒有問題。任何不明確的部分都 要附有說明。

保證邏輯的清晰性要「不擇手段」。在無法用代碼證明邏輯正確性的情況下,我們也可以通過寫注釋、附文檔或畫圖等方法來證明。不過,證明邏輯的正確性是一件麻煩的事,時間一長,人們就會懶得用輔助手段去證明,轉而編寫邏輯清晰的代碼了。

Why:消除不確定性
代碼免不了被人一遍又一遍地閱讀,所以代碼必須保持較高的可讀性。編寫代碼時如果追求高可讀性,我們就不會採用取巧的方式編寫代碼,編寫出的代碼會非常自然。

採用取巧的方式編寫的代碼除了能讓計算機運行以外沒有任何意義。代碼是給人看的,也是由人來修改的,所以我們必須以人為對象來編寫代碼。

消除代碼的不確定性是對自己的作品負責,這麼做也可以為後續負責維護的人提供方便。

Do:編寫邏輯清晰的代碼
我們要編寫邏輯清晰的代碼。

為此,我們應選用直觀易懂的邏輯。會給讀代碼的人帶來疑問的部分要麼消除,要麼加以注釋。

另外,我們應使用任何人都能立刻理解且不存在歧義的術語。要特別注意變量名等一定不能沒有意義。

07
安全原則
Safty Principle
What:注意安全性
安全原則就是注意安全性,採用相對安全的方法來對具有不確定性的、模糊的部分進行設計和編程。

說得具體一點,就是在編寫代碼時刻意將不可能的條件考慮進去。比如即便某個 i f 語句一定成立,我們也要考慮 else 語句的情況;即便某個 case 語句一定成立,我們也要考慮 default 語句的情況;即便某個變量不可能為空,我們也要檢查該變量是否為 NULL。

Why:防止故障發展成重大事故
硬件提供的服務必須保證安全,軟件也一樣。

硬件方面,比如取暖器,為防止傾倒起火,取暖器一般會配有傾倒自動斷電裝置。同樣,設計軟件時也需要考慮各種情況,保證軟件在各種情況下都能安全地運行。這一做法在持續運營服務和防止數據損壞等方面有著積極的意義。

Do:編寫安全的代碼
選擇相對安全的方法對具有不確定性的部分進行設計。列出所有可能的運行情況,確保軟件在每種情況下都能安全運行。理解需求和功能,將各種情況正確分解到代碼中,這樣能有效提高軟件安全運行的概率。
為此,我們也要將不可能的條件視為考察對象,對其進行設計和編程。不過,為了統一標準,我們在編寫代碼前最好規定哪些條件需要寫,哪些條件不需要寫。

KISS (Keep it simple and stupid)
一般大腦工作記憶的容量就是 5-9 個,如果事情過多或者過於複雜,對於大部分人來說是無法直接理解和處理的。通常我們需要一些輔助手段來處理複雜的問題,比如做筆記、畫圖,有點類似於在內存不夠用的情況下我們借用了外存。
學 CS 的同學都知道,外存的訪問速度肯定不如內存訪問速度。另外一般來說在邏輯複雜的情況下出錯的可能要遠大於在簡單的情況下,在複雜的情況下,代碼的分支可能有很多,我們是否能夠對每種情況都考慮到位,這些都有困難。
為了使得代碼更加可靠,並且容易理解,最好的辦法還是保持代碼的簡單,在處理一個問題的時候盡量使用簡單的邏輯,不要有過多的變量。
但是現實的問題並不會總是那麼簡單,那麼如何來處理複雜的問題呢?與其借用外存,我更加傾向於對複雜的問題進行分層抽象。
網絡的通信是一個非常複雜的事情,中間使用的設備可以有無數種(手機,各種 IOT 設備,台式機,laptop,路由器,交換機...),OSI 協議對各層做了抽象,每一層需要處理的情況就都大大地簡化了。
通過對複雜問題的分解、抽象,那麼我們在每個層次上要解決處理的問題就簡化了。其實也類似於算法中的 divide-and-conquer, 複雜的問題,要先拆解掉變成小的問題,從而來簡化解決的方法。
KISS 還有另外一層含義,「如無必要,勿增實體」 (奧卡姆剃刀原理)。CS 中有一句 "All problems in computer science can be solved by another level of indirection", 為了系統的擴展性,支持將來的一些可能存在的變化,我們經常會引入一層間接層,或者增加中間的 interface。
在做這些決定的時候,我們要多考慮一下是否真的有必要。增加額外的一層給我們的好處就是易於擴展,但是同時也增加了複雜度,使得系統變得更加不可理解。
對於代碼來說,很可能是我這裡調用了一個 API,不知道實際的觸發在哪裡,對於理解和調試都可能增加困難。
KISS 本身就是一個 trade off,要把複雜的問題通過抽象和分拆來簡單化,但是是否需要為了保留變化做更多的 indirection 的抽象,這些都是需要仔細考慮的。
DRY (Don't repeat yourself)
為了快速地實現一個功能,知道之前有類似的,把代碼 copy 過來修改一下就用,可能是最快的方法。但是 copy 代碼經常是很多問題和 bug 的根源。
有一類問題就是 copy 過來的代碼包含了一些其他的邏輯,可能並不是這部分需要的,所以可能有冗余甚至一些額外的風險。
另外一類問題就是在維護的時候,我們其實不知道修復了一個地方之後,還有多少其他的地方還需要修復。
在我過去的項目中就出現過這樣的問題,有個問題明明之前做了修復,過幾天另外一個客戶又提了類似的問題出現的另外的路徑上。
相同的邏輯要盡量只出現在一個地方,這樣有問題的時候也就可以一次性地修復。這也是一種抽象,對於相同的邏輯,抽象到一個類或者一個函數中去,這樣也有利於代碼的可讀性。