程序是寫給人讀的,只是偶爾讓計算機執行一下。 —— Donald Ervin Knuthgit
每次 review 過往寫的代碼,總有一種不忍直視的感受。想提升編碼能力,故閱讀了一些相關書籍及博文,並有所感悟,今將一些讀書筆記及我的心得感悟梳理出來。拋轉引玉,但願這磚能拋得起來。程序員
開始閱讀以前,你們能夠快速思考一下,你們腦海裏的好代碼和壞代碼都是怎麼樣的「形象」呢?github
若是看到這一段代碼,如何評價呢?算法
if (a && d || b && c && !d || (!a || !b) && c) { // ... } else { // ... } 複製代碼
上面這段代碼,儘管是特地爲舉例而寫的,要是真實遇到這種代碼,想必你們都「一言難盡」吧。你們多多少少都有一些壞味道的代碼的「印象」,壞味道的代碼總有一些共性:編程
那壞味道的代碼是怎樣造成的呢?設計模式
對壞味道的代碼有一個大概的瞭解後,或許讀者心中有一個疑問:代碼的好壞有沒有一些量化的標準去評判呢?答案是確定的。bash
接下來,經過了解圈複雜度去衡量咱們寫的代碼。然而當代碼的壞味道已經「瀰漫」處處都是了,這時咱們應該瞭解一下重構。代碼到了咱們手裏,不能繼續「發散」壞味道,這時應該瞭解如何編寫 clean code。此外,咱們還應該掌握一些編碼原則及設計模式,這樣才能作到有的放矢。markdown
圈複雜度(Cyclomatic complexity,簡寫CC)也稱爲條件複雜度,是一種代碼複雜度的衡量標準。由托馬斯·J·麥凱布(Thomas J. McCabe, Sr.)於1976年提出,用來表示程序的複雜度。架構
圈複雜度能夠用來衡量一個模塊斷定結構的複雜程度,數量上表現爲獨立現行路徑條數,也可理解爲覆蓋全部的可能狀況最少使用的測試用例數。app
圈複雜度能夠經過程序控制流圖計算,公式爲:
V(G) = e + 2 - n
有一個簡單的計算方法:圈複雜度實際上就是等於斷定節點的數量再加上1。
注:
if else
、switch case
、for循環
、三元運算符
、||
、&&
等,都屬於一個斷定節點。
代碼複雜度低,代碼不必定好,但代碼複雜度高,代碼必定很差。
圈複雜度 | 代碼情況 | 可測性 | 維護成本 |
---|---|---|---|
1 - 10 | 清晰、結構化 | 高 | 低 |
10 - 20 | 複雜 | 中 | 中 |
20 - 30 | 很是複雜 | 低 | 高 |
>30 | 不可讀 | 不可測 | 很是高 |
ESLint 提供了檢測代碼圈複雜度的 rules。開啓 rules 中的 complexity 規則,並將圈複雜度大於 0 的代碼的 rule severity 設置爲 warn 或 error 。
rules: { complexity: [ 'warn', { max: 0 } ] } 複製代碼
藉助 ESLint 的 CLIEngine ,在本地使用自定義的 ESLint 規則掃描代碼,並獲取掃描結果輸出。
不少狀況下,下降圈複雜度就能提升代碼的可讀性了。針對圈複雜度,結合例子給出一些改善的建議:
經過抽象配置將複雜的邏輯判斷進行簡化。
before:
// ... if (type === '掃描') { scan(args) } else if (type === '刪除') { delete(args) } else if (type === '設置') { set(args) } else { // ... } 複製代碼
after:
const ACTION_TYPE = { 掃描: scan, 刪除: delete, 設置: set } ACTION_TYPE[type](args) 複製代碼
將代碼中的邏輯進行抽象提煉成單獨的函數,有利於下降代碼複雜度和下降維護成本。尤爲是當一個函數的代碼很長,讀起來很費力的時候,就應該思考可否提煉成多個函數。
before:
function example(val) { if (val > MAX_VAL) { val = MAX_VAL } for (let i = 0; i < val; i++) { doSomething(i) } // ... } 複製代碼
after:
function setMaxVal(val) { return val > MAX_VAL ? MAX_VAL : val } function getCircleArea(val) { for (let i = 0; i < val; i++) { doSomething(i) } } function example(val) { return getCircleArea(setMaxVal(val)) } 複製代碼
某些複雜的條件判斷可能逆向思考後會變的更簡單,還能減小嵌套。
before:
function checkAuth(user){ if (user.auth) { if (user.name === 'admin') { // ... } else if (user.name === 'root') { // ... } } } 複製代碼
after:
function checkAuth(user){ if (!user.auth) return if (user.name === 'admin') { // ... } else if (user.name === 'root') { // ... } } 複製代碼
將冗餘的條件合併,而後再進行判斷。
before:
if (fruit === 'apple') { return true } else if (fruit === 'cherry') { return true } else if (fruit === 'peach') { return true } else { return true } 複製代碼
after:
const redFruits = ['apple', 'cherry', 'peach'] if (redFruits.includes(fruit) { return true } 複製代碼
對複雜難懂的條件進行提取並語義化。
before:
if ((age < 20 && gender === '女') || (age > 60 && gender === '男')) { // ... } else { // ... } 複製代碼
after:
function isYoungGirl(age, gender) { return (age < 20 && gender === '女' } function isOldMan(age, gender) { return age > 60 && gender === '男' } if (isYoungGirl(age, gender) || isOldMan(age, gender)) { // ... } else { // ... } 複製代碼
後文有簡化條件表達式更全面的總結。
重構一詞有名詞和動詞上的理解。名詞:
對軟件內部結構的一種調整,目的是在不改變軟件可觀察行爲的前提下,提升其可理解性,下降其修改爲本。
動詞:
使用一系列重構手法,在不改變軟件可觀察行爲的前提下,調整其結構。
若是遇到如下的狀況,可能就要思考是否須要重構了:
爲什麼重構,不外乎如下幾點:
本文討論的內容只涉及第一點,僅限代碼級別的重構。
第一次作某件事時只管去作;第二次作相似的事會產生反感,但不管如何仍是能夠去作;第三次再作相似的事,你就應該重構。
關鍵思想:一致的風格比「正確」的風格更重要。
原則:
註釋的目的是儘可能幫助讀者瞭解得和做者同樣多。所以註釋應當有很高的信息/空間
率。
標記 | 一般的意義 |
---|---|
TODO: | 還沒處理的事情 |
FIXME: | 已知的沒法運行的代碼 |
HACK: | 對一個問題不得不採用的比價粗糙的解決方案 |
關鍵思想:把信息裝入名字中。
良好的命名是一種以「低代價」取得代碼高可讀性的途徑。
「把信息裝入名字中」包括要選擇很是專業的詞,而且避免使用「空洞」的詞。
單詞 | 更多選擇 |
---|---|
send | deliver, despatch, announce, distribute, route |
find | search, extract, locate, recover |
start | launch, create, begin, open |
make | create, set up, build, generate, compose, add, new |
在給變量、函數或者其餘元素命名時,要把它描述得更具體而不是更抽象。
若是關於一個變量有什麼重要事情的讀者必須知道,那麼是值得把額外的「詞」添加到名字中的。
正 | 反 |
---|---|
add | remove |
create | destory |
insert | delete |
get | set |
increment | decrement |
show | hide |
start | stop |
有一個複雜的條件(if-then-else)語句,從if、then、else三個段落中分別提煉出獨立函數。根據每一個小塊代碼的用途,爲分解而獲得的新函數命名,並將原函數中對應的代碼改成調用新建函數,從而更清楚地表達本身的意圖。對於條件邏輯,能夠突出條件邏輯,更清楚地代表每一個分支的做用,而且突出每一個分支的緣由。
有一系列條件測試,都獲得相同結果。將這些測試合併爲一個條件表達式,並將這個條件表達式提煉成爲一個獨立函數。
在條件表達式的每一個分支上有着相同的一段代碼,將這段重複代碼搬移到條件表達式以外。
函數中的條件邏輯令人難以看清正常的執行路徑。使用衛語句表現全部特殊狀況。
若是某個條件極其罕見,就應該單獨檢查該條件,並在該條件爲真時馬上從函數中返回。這樣的單獨檢查經常被稱爲「衛語句」(guard clauses)。
經常能夠將條件表達式反轉,從而實以衛語句取代嵌套條件表達式,寫成更加「線性」的代碼來避免深嵌套。
變量存在的問題:
若是有一個臨時變量,只是被簡單表達式賦值一次,而將全部對該變量的引用動做,替換爲對它賦值的那個表達式自身。
以一個臨時變量保存某一表達式的運算結果,將這個表達式提煉到一個獨立函數中。將這個臨時變量的全部引用點替換爲對新函數的調用。此後,新函數就可被其餘函數使用。
接上條,若是該表達式比較複雜,建議經過一個總結變量名來代替一大塊代碼,這個名字會更容易管理和思考。
將複雜表達式(或其中一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式用途。
在條件邏輯中,引入解釋性變量特別有價值:能夠將每一個條件子句提煉出來,以一個良好命名的臨時變量來解釋對應條件子句的意義。使用這項重構的另外一種狀況是,在較長算法中,能夠運用臨時變量來解釋每一步運算的意義。
好處:
程序有某個臨時變量被賦值超過一次,它既不是循環變量,也不是用於收集計算結果。針對每次賦值,創造一個獨立、對應的臨時變量。
臨時變量有各類不一樣用途:
若是臨時變量承擔多個責任,它就應該被替換(分解)爲多個臨時變量,每一個變量只承擔一個責任。
有一個字面值,帶有特別含義。創造一個常量,根據其意義爲它命名,並將上述的字面數值替換爲這個常量。
let done = false; while (condition && !done) { if (...) { done = true; continue; } } 複製代碼
像done這樣的變量,稱爲「控制流變量」。它們惟一的目的就是控制程序的執行,沒有包含任何程序的數據。控制流變量一般能夠經過更好地運用結構化編程而消除。
while (condition) { if (...) { break; } } 複製代碼
若是有多個嵌套循環,一個簡單的break不夠用,一般解決方案包括把代碼挪到一個新函數中。
當一個過長的函數或者一段須要註釋才能讓人理解用途的代碼,能夠將這段代碼放進一個獨立函數中。
一個函數過長才合適?長度不是問題,關鍵在於函數名稱和函數本體之間的語義距離。
某個函數既返回對象狀態值,又修改對象狀態。創建兩個不一樣的函數,其中一個負責查詢,另外一個負責修改。
有一個函數,其中徹底取決於參數值而採起不一樣行爲。針對該參數的每個可能值,創建一個獨立函數。
某些參數老是很天然地同時出現,以一個對象取代這些參數。
能夠經過立刻處理「特殊狀況」,並從函數中提早返回。
若是有很難讀的代碼,嘗試把它所作的全部任務列出來。其中一些任務能夠很容易地變成單獨的函數(或類)。其餘的能夠簡單地成爲一個函數中的邏輯「段落」。
在循環中,與提前返回相似的技術是continue
。與if...return;
在函數中所扮演的保護語句同樣,if...continue;
語句是循環中的保護語句。(注意:JavaScript 中 forEach 的特殊性)
對於一個布爾表達式,有兩種等價寫法:
可使用這些法則讓布爾表達式更具備可讀性。例如:
// before if (!(file_exitsts && !is_protected)) Error("sorry, could not read file.") // after if (!file_exists || is_protected) Error("sorry, could not read file.") 複製代碼
使用相關定律能優化開始舉例的那段代碼:
// before if (a && d || b && c && !d || (!a || !b) && c) { // ... } else { // ... } // after if (a && d || c) { // ... } else { // ... } 複製代碼
具體簡化過程及涉及相關定律能夠參考這篇推文:你寫這樣的代碼,不怕同事打你嘛?
所謂工程學就是關於把大問題拆分紅小問題再把這些問題的解決方案放回一塊兒。
把這條原則應用於代碼會使代碼更健壯而且更容易讀。
積極地發現並抽取不相關的子邏輯,是指:
若是你不能把一件事解釋給你祖母聽的話說明你還沒真正理解它。 --阿爾伯特·愛因斯坦
步驟以下:
有必要熟知前人總結的一些經典的編碼原則及涉及模式,以此來改善咱們既有的編碼習慣,所謂「站在巨人肩上編程」。
SOLID 是面向對象設計(OOD)的五大基本原則的首字母縮寫組合,由俗稱「鮑勃大叔」的Robert C.Martin在《敏捷軟件開發:原則、模式與實踐》一書中提出來。
A class should have only one reason to change.
一個類應該有且僅有一個緣由引發它的變動。
通俗來說:一個類只負責一項功能或一類類似的功能。固然這個「一」並非絕對的,應該理解爲一個類只負責儘量獨立的一項功能,儘量少的職責。
優勢:
缺點:
這條定律一樣適用於組織函數時的編碼原則。
Software entities (classes,modules,functions,etc.)should be openfor extension,but closed for modification.
軟件實體(如類、模塊、函數等)應該對拓展開放,對修改封閉。
在一個軟件產品的生命週期內,不可避免會有一些業務和需求的變化,咱們在設計代碼的時候應該儘量地考慮這些變化。在增長一個功能時,應當儘量地不去改動已有的代碼;當修改一個模塊時不該該影響到其餘模塊。
const makeSound = function( animal ) { animal.sound(); }; const Duck = function(){}; Duck.prototype.sound = function(){ console.log( '嘎嘎嘎' ); }; const Chicken = function(){}; Chicken.prototype.sound = function(){ console.log( '咯咯咯' ); }; makeSound( new Duck() ); // 嘎嘎嘎 makeSound( new Chicken() ); // 咯咯咯 複製代碼
Functions that use pointers to base classes must be able to useobjects of derived classes without knowing it.
全部能引用基類的地方必須能透明地使用其子類的對象。
只要父類能出現的地方子類就能出現(就能夠用子類來替換它)。反之,子類能出現的地方父類不必定能出現(子類擁有父類的全部屬性和行爲,但子類拓展了更多的功能)。
Clients should not be forced to depend upon interfaces that they don't use.Instead of one fat interface many small interfaces arepreferred based on groups of methods,each one serving onesubmodule.
客戶端不該該依賴它不須要的接口。用多個細粒度的接口來替代由多個方法組成的複雜接口,每個接口服務於一個子模塊。
接口儘可能小,可是要有限度。當發現一個接口過於臃腫時,就要對這個接口進行適當的拆分。可是若是接口太小,則會形成接口數量過多,使設計複雜化。
High level modules should not depend on low level modules; bothshould depend on abstractions.Abstractions should not depend ondetails.Details should depend upon abstractions.
高層模塊不該該依賴低層模塊,兩者都該依賴其抽象。抽象不該該依賴細節,細節應該依賴抽象。
把具備相同特徵或類似功能的類,抽象成接口或抽象類,讓具體的實現類繼承這個抽象類(或實現對應的接口)。抽象類(接口)負責定義統一的方法,實現類負責具體功能的實現。
沒有這麼充足的時間遵循這些原則去設計,或遵循這些原則設計的實現成本太大。在受現實條件所限不能遵循五大原則來設計時,咱們還能夠遵循下面這些更爲簡單、實用的原則。
Each unit should have only limited knowledge about other units: onlyunits "closely"related to the current unit.Only talk to your immediatefriends,don't talk to strangers.
每個邏輯單元應該對其餘邏輯單元有最少的瞭解:也就是說只親近當前的對象。只和直接(親近)的朋友說話,不和陌生人說話。
這一原則又稱爲迪米特法則,簡單地說就是:一個類對本身依賴的類知道的越少越好,這個類只須要和直接的對象進行交互,而不用在意這個對象的內部組成結構。
例如,類A中有類B的對象,類B中有類C的對象,調用方有一個類A的對象a,這時若是要訪問C對象的屬性,不要採用相似下面的寫法:
a.getB().getC().getProperties()
複製代碼
而應該是:
a.getProperties()
複製代碼
Keep It Simple and Stupid.
保持簡單和愚蠢。
DRY原則(Don't Repeat Yourself)
不要重複本身。
不要重複你的代碼,即屢次遇到一樣的問題,應該抽象出一個共同的解決方法,不要重複開發一樣的功能。也就是要儘量地提升代碼的複用率。
要遵循DRY原則,實現的方式很是多:
DRY原則在單人開發時比較容易遵照和實現,但在團隊開發時不太容易作好,特別是對於大團隊的項目,關鍵仍是團隊內的溝通。
You aren't gonna need it,don't implement something until it isnecessary.
你不必那麼着急,不要給你的類實現過多的功能,直到你須要它的時候再去實現。
Rule of three 稱爲「三次法則」,指的是當某個功能第三次出現時,再進行抽象化,即事不過三,三則重構。
保證方法的行爲嚴格的是命令或者查詢,這樣查詢方法不會改變對象的狀態,沒有反作用;而會改變對象的狀態的方法不可能有返回值。
設計模式的開山鼻祖 GoF 在《設計模式:可複用面向對象軟件的基礎》一書中提出的23種經典設計模式被分紅了三類,分別是建立型模式、結構型模式和行爲型模式。
在面向對象軟件設計過程當中針對特定問題的簡潔而優雅的解決方案。
經常使用的設計模式有:策略模式、發佈—訂閱模式、職責鏈模式等。
好比策略模式使用的場景:
策略模式:定義一系列的算法,把它們一個個封裝起來,而且使它們能夠相互替換。
if (account === null || account === '') { alert('手機號不能爲空'); return false; } if (pwd === null || pwd === '') { alert('密碼不能爲空'); return false; } if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) { alert('手機號格式錯誤'); return false; } if(pwd.length<6){ alert('密碼不能小於六位'); return false; } 複製代碼
使用策略模式:
const strategies = { isNonEmpty: function (value, errorMsg) { if (value === '' || value === null) { return errorMsg; } }, isMobile: function (value, errorMsg) { // 手機號碼格式 if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) { return errorMsg; } }, minLength: function (value, length, errorMsg) { if (value.length < length) { return errorMsg; } } }; const accountIsMobile = strategies.isMobile(account,'手機號格式錯誤'); const pwdMinLength = strategies.minLength(pwd,8,'密碼不能小於8位'); const errorMsg = accountIsMobile || pwdMinLength; if (errorMsg) { alert(errorMsg); return false; } 複製代碼
又好比,發佈—訂閱模式具備的特色:
既能夠用在異步編程中,也能夠幫助咱們完成更鬆耦合的代碼編寫。
若是你們須要瞭解設計模式更多知識,建議另外找資料學習。
宋代禪宗大師青原行思提出參禪的三重境界:
參禪之初,看山是山,看水是水;禪有悟時,看山不是山,看水不是水;禪中徹悟,看山還是山,看水還是水。
同理,編程一樣存在境界:編程的一重境界是照葫蘆畫瓢,二重境界是能夠靈活運用,三重境界則是心中無模式。惟有多實踐,多感悟,方能突破一重又一重的境界。
最後,願你們終將能寫出本身再也不討厭的代碼。
真的是最後了,有空會補上上述的示例代碼,歡迎你們 Star & PR 呀:你所須要知道的代碼整潔之道