程式碼質量管理——如何寫出優雅的程式碼

歪程式喵發表於2018-02-26

作為一個剛寫程式碼不久的小菜鳥,工作的半年多讓我越發意識到提高程式碼質量的重要性。從前只會關注實現功能,慢慢的開始關注效能,現階段則發現其實還有很多細節也是(如可讀性、易用性、可維護性、一致性)提高程式碼質量的關鍵。“實現功能”跟“優雅地實現功能”是兩碼事。

注:大部分內容歸納自網路,將多篇文章的觀點彙總加工了一下,也融合了一些個人的見解。

原則

  • 單一職責原則

  • 易用性原則

  • 可讀性原則

  • 複雜性守恆原則:無論你怎麼寫程式碼,複雜性都是不會消失的

    注:如果邏輯很複雜,那麼程式碼看起來就應該是複雜的。如果邏輯很簡單,程式碼看起來就應該是簡單的。

單一職責原則

物件導向五大設計模式基本原則之一。即一部分程式碼只應該用於某一個特定功能,不應與其他功能耦合在一起

假設你的一個function同時實現了功能a和功能b,之後需求變更,你需要修改功能a,但是因為這兩個功能都在一個function裡,你就不得不再去確認是否會影響到功能b。這就造成了不必要的成本。

如下我總結了三個拆分程式碼的原則:

“and”原則

當你為你的方法命名時不得不加上“and”時,就該考慮考慮是不是要把這個方法拆分一下了。

“100”原則

當你的一個function超過一百行時,一定要進行拆分了。

注:這裡的100可能有點多,只是對我個人而言,100算是我的極限,總之就是絕對不要將一個函式寫的太長。

命令、查詢拆分原則

我們開發中大部分操作可以總結為“命令”和“查詢”,如寫cookie、修改data、傳送post請求都可以叫“命令”,而讀取cookie、ajax獲取資料則認為是“查詢”操作。

函數語言程式設計中講究“資料不可變”,即:

只有純的沒有副作用的函式,才是合格的函式。

副作用:指當呼叫函式時,除了返回函式值之外,還對主呼叫函式產生附加的影響。例如修改全域性變數(函式外的變數)或修改引數。

好處是使得開發更加簡單,可回溯,測試友好,減少了任何可能的副作用。

將“命令”與“查詢”拆分實際上就是函數語言程式設計思想的部分體現,參考如下程式碼:

function getFirstName() {
    var firstName = document.querySelector("#firstName").value;
    firstName = firstName.toLowerCase();
    setCookie("firstName", firstName);
    if (firstName === null) {
        return "";
    }
    return firstName;
}
 
var activeFirstName = getFirstName();
複製程式碼

通過名字來看,該方法是用於獲取first name的,但實際上它還設定了cookie,這是我們沒有預料的。對於一個“查詢”方法,它不應該有任何修改方法外變數的行為,即“副作用”。更好的寫法如下:

function getFirstName() {
    var firstName = document.querySelector("#firstName").value
    if (firstName === null) {
        return "";
    }
    return firstName;
}
 
setCookie("firstName", getFirstName().toLowerCase());
複製程式碼

一目瞭然,getFirstName只返回firstName,而設定cookie操作在它之外進行。

易用性原則

簡單

這裡的簡單,主要歸結為function的一些設計原則,有如下幾點:呼叫簡單、易理解、減少記憶成本、引數處理。

如下,僅僅想實現修改dom顏色、寬度等屬性,原生程式碼如下:

document.querySelector('#id').style.color = 'red'
document.querySelector('#id').style.width = '123px'
document.querySelector('#id').style.height = '456px'
複製程式碼

而封裝過後:

function a(selector, color, width, height) {
  document.querySelector(selector).style.color = color
  document.querySelector(selector).style.width = width
  document.querySelector(selector).style.height = height
}
a('#a', 'red')
複製程式碼

瞬間變得簡單可用了 ~

但該方法還存在一個問題,那就是命名太抽象了。。。除了開發者自己以外不可能有人在不看原始碼的情況下一眼看出這個方法a是幹嘛的。那麼我們再把這個方法名改寫得更易理解一點:

function letSomeElementChange(selector, color, width, height) {
  document.querySelector(selector).style.color = color
  document.querySelector(selector).style.width = width
  document.querySelector(selector).style.height = height
}
複製程式碼

這樣我們就能一目瞭然該方法的作用 ~ 不過仍有可優化的地方。這麼長的方法名誰記得住,要減少記憶成本啊,再改個名:

function setElement(selector, color, width, height) {
  document.querySelector(selector).style.color = color
  document.querySelector(selector).style.width = width
  document.querySelector(selector).style.height = height
}
複製程式碼

OK,目前這個方法已經滿足它的職責並且很好用了,但還覺得怪怪的。這一坨引數太礙眼。。。

function setElement(selector, opt) {
  const { color, width, height } = opt
  color && document.querySelector(selector).style.color = color
  width && document.querySelector(selector).style.width = width
  height && document.querySelector(selector).style.height = height
}
複製程式碼

把多個引數合併一下,並在內部做相容處理,這個方法便易用多了。即使不傳第二個引數也不會有任何副作用。

一致性

假如有這樣一個方法,獲取歌曲列表,並將其設定到div的innerText中:

function getSongs() {
    return $.get('/songs).then((response) {
        div.innerText = response.songs
  })
}
複製程式碼

這就違背了方法的表裡一致性,也違背了上文的單一職責原則命令、查詢拆分原則,因為它不僅獲取了歌單,同時還修改了innerText,要讓其更合理:

  • 要麼換個名字
  • 要麼拆分為兩個方法

低耦合

耦合是衡量一個程式單元對其他程式單元的依賴程度。耦合(或高耦合)是應該極力避免的。如果你發現自己正在複製和貼上程式碼並進行小的更改,或者重寫程式碼,因為其他地方發生了更改,這就是高耦合的體現。

耦合會嚴重影響程式碼的複用性及可擴充套件性,讓後人維護時不得不修改甚至重寫這部分程式碼,不僅浪費時間還會導致倉儲中又多出一塊類似的程式碼,很容易讓人迷惑。

同時,修改耦合度高的程式碼時經常會牽一髮而動全身,如果修改時沒有理清這些耦合關係,那麼帶來的後果可能會是災難性的,特別是對於需求變化較多以及多人協作開發維護的專案,修改一個地方會引起本來已經執行穩定的模組錯誤,嚴重時會導致惡性迴圈,問題永遠改不完,開發和測試都在各種問題之間奔波勞累,最後導致專案延期,使用者滿意度降低,成本也增加了,這對使用者和開發商影響都是很惡劣的,各種風險也就不言而喻了。

高內聚

不應該將沒有任何聯絡的東西堆到一起。

內聚是一個類中變數與方法連線強度的尺度。高內聚是值得要的,因為它意味著類可以更好地執行一項工作。低內聚是不好的,因為它表明類中的元素之間很少相關。每個方法也應該高內聚,大多數的方法只執行一個功能,不要在方法中新增‘額外’的指令,這樣會導致方法執行更多的函式,同時也違反了上文的單一職責原則

低內聚的體現:如果屬性沒有被類中的多個方法使用,這可能是低內聚的標誌。同樣,如果方法在幾種不同的情況下不能被重用,或者如果一個方法根本不被使用,這也可能是低內聚的一個標誌。

高內聚有助於緩解高耦合,高耦合是需要高內聚的標誌。但是,如果兩個問題同時存在,應當選擇內聚的方式。對於開發者來說,高內聚通常比低耦合更有幫助,儘管兩者通常可以一起完成。

錯誤處理

  • 可預見的錯誤:諸如ajax回撥、函式引數,這類問題很好解決,只需在開發時多考慮一步,對各種極端情況做好相容即可。
  • 不可預見的錯誤:類似相容性問題,這類問題無法在開發時準確預見的錯誤,可以準備好拋錯,console.error/log/warn,最後你還可以為自己的程式留些後路: try...catch。

可讀性原則

命名

命名應該保證別人通過名稱一眼就能知道這個變數儲存的是什麼,或者這個方法是用來做什麼的。

  • 普通變數、屬性用名詞如下:

    var person = {
        name: 'Frank'
    }
    var student = {
        grade: 3,
        class: 2
    }
    複製程式碼
  • bool變數、屬性用(形容詞)或者(be動詞)或者(情態動詞)或者(hasX),如下:

    var person = {
        dead: false, // 如果是形容詞,前面就沒必要加 is,比如isDead 就很廢話
        canSpeak: true, //情態動詞有 can、should、will、need 等,情態動詞後面接動詞
        isVip: true, // be 動詞有 is、was 等,後面一般接名詞
        hasChildren: true, // has 加名詞
    }
    複製程式碼
  • 普通函式、方法用(動詞)開頭:

    var person = {
        run(){}, // 不及物動詞
        drinkWater(){}, // 及物動詞
        eat(foo){}, // 及物動詞加引數(引數是名詞)
    }
    複製程式碼
  • 回撥、鉤子函式:

    var person = {
        beforeDie(){},
        afterDie(){},
        // 或者
        willDie(){}
        dead(){} // 這裡跟 bool 衝突,你只要不同時暴露 bool dead 和函式 dead 就行,怕衝突就用上面的 afterDie
    }
    button.addEventListener('click', onButtonClick)
    var component = {
        beforeCreate(){},
        created(){},
        beforeMount(){}
    }
    複製程式碼
  • 命名一致性

    • 順序一致性:比如 updateContainerWidth 和 updateHeightOfContainer 的順序就令人很彆扭
    • 時間一致性:有可能隨著程式碼的變遷,一個變數的含義已經不同於它一開始的含義了,這個時候你需要及時改掉這個變數的名字。 這一條是最難做到的,因為寫程式碼容易,改程式碼難。如果這個程式碼組織得不好,很可能會出現牽一髮而動全身的情況(如全域性變數就很難改)

註釋

不需要多花哨,只要把作用、用法描述清楚即可。方法的標準註釋應該如下:

/**
 * [function_name description]
 * @param  {[type]} argument [description]
 * @return {[type]}          [description]
 */
function function_name(argument) {
  // body...
}
複製程式碼

將方法的引數與返回值都寫清楚,我目前用的IDE是sublime,使用Docblockr外掛可以自動生成格式化註釋,很方便。

Bad Smell

專案中我們經常能夠遇這類程式碼,它們仍可用,但是很“臭”,國外管這類程式碼有一個統稱,即“bad smell”。如下這類程式碼可以說是很“臭”了:

  • 表裡不一的程式碼
  • 過時的註釋
  • 邏輯很簡單,但是看起來很複雜的程式碼
  • 重複的程式碼
  • 相似的程式碼
  • 總是一起出現的程式碼
  • 未使用的依賴
  • 不同風格的程式碼

樣式規範

  1. 正確命名:class必須用“-”寫法,不要用駝峰和下劃線。

  2. 正確巢狀:正常情況下一定要將class巢狀閉合,否則就相當於新增到全域性,如果有重複命名的class就會受影響。

  3. 拒絕copy:如果想複用已有的樣式,直接在原有class上用“,”語法分割,就能應用,不要再copy一份樣式,會讓兩份樣式都被應用,就要考慮樣式覆蓋的問題,很不友好。

  4. 濫用class:沒有必要加的class不要加,每個class的新增都應該有明確理由。濫用class的話可能會導致樣式覆蓋,不該應用這個樣式的地方用了某個樣式。

  5. 慎用 !important,會強行覆蓋所有同屬性樣式,一旦使用後會讓程式碼難以維護,開發過程中絕對不要依賴該方法。如下總結了一些使用 !important的經驗:

    • 一定要優化考慮使用樣式規則的優先順序來解決問題而不是 !important
    • 只有在需要覆蓋全站或外部 css(例如引用的 ExtJs 或者 YUI )的特定頁面中使用 !important
    • 解決緊急線上問題可以使用,但之後也要儘快用可維護的方式將程式碼替換回來
    • 永遠不要在全站範圍的 css 上使用 !important
    • 永遠不要在你的外掛中使用 !important

說得容易,做起來難

破窗效應

此理論認為環境中的不良現象如果被放任存在,會誘使人們仿效,甚至變本加厲。一幢有少許破窗的建築為例,如果那些窗不被修理好,可能將會有破壞者破壞更多的窗戶。最終他們甚至會闖入建築內,如果發現無人居住,也許就在那裡定居或者縱火。一面牆,如果出現一些塗鴉沒有被清洗掉,很快的,牆上就佈滿了亂七八糟、不堪入目的東西;一條人行道有些許紙屑,不久後就會有更多垃圾,最終人們會視若理所當然地將垃圾順手丟棄在地上。這個現象,就是犯罪心理學中的破窗效應,在程式設計領域同樣存在。

要做到:只要是經過你手的程式碼,都會比之前好一點。

最後弱弱地打個廣告,我的部落格,歡迎各路大神指教

參考文章:

相關文章