[譯] 我在程式設計初級階段常犯的錯誤

kezhenxu94發表於2018-05-02

學會辨識錯誤,養成習慣去避免它們

[譯] 我在程式設計初級階段常犯的錯誤

我要先宣告一點,如果你是一個初級程式設計師,本文並非要讓你因為可能正在犯這些錯感到藍瘦香菇,而是要讓你意識到它們的存在,教你如何辨識它們,並且提醒你避免犯這些錯。

過去我經常犯這類錯誤,從每一個錯誤中我都吸取了很多教訓。可喜的是如今我已經養成了很好的程式設計習慣,這些習慣能幫我避免再次犯同樣的錯。你也應該嘗試著去這樣做。

以下錯誤排名不分先後。

1)毫無計劃地寫程式碼

高質量的寫作內容大都不那麼容易產出,它要求仔細的思考和研究。高質量的程式程式碼也不例外。

編寫高質量程式碼是有一個工作流的: 思考調研計劃編碼驗證修改。 很遺憾,這個工作流沒有一個很好的(英文)首字母縮寫來幫助記憶。你需要養成好的習慣來履行工作流中的各個階段,一個都不能少

在我程式設計初級階段的時候,我犯過最嚴重的錯誤之一就是寫程式碼之前沒有思考和調研。雖然這在小型獨立的專案中能夠奏效,但在更大的工程中就會有很嚴重的負面影響了。

正如在說出可能會後悔的話之前需要三思一樣,在你寫出可能會後悔的程式碼之前也需要三思。程式碼也是交流思想的一種方式。

生氣的時候,如果要說話,先從 1 數到 10。如果非常生氣,那就數到 100。

— Thomas Jefferson(譯註:托馬斯 傑佛遜,美國第三任總統)

套用一下這句話:

複審程式碼的時候,如果要重構程式碼,先從 1 數到 10。如果沒有測試程式碼,那就數到 100。

— Samer Buna(譯註:本文作者)

程式設計大部分情況下都是關於如何閱讀既有程式碼,調研新需求及其如何適應當前系統,以及規劃如何使用可測試的增量程式碼實現新功能。實際寫程式碼的時間在整個過程中可能才佔比 10% 而已。

不要覺得程式設計就是寫一行一行的程式碼。程式設計是一個有邏輯的創造過程,是需要培養教育的。

2)寫程式碼前過度計劃

是的。在一頭鑽進程式碼前做點計劃是好事,但是即便是好事,也可能物極必反。喝太多的水都會使你中毒呢。

在程式設計的世界裡尋找完美的計劃?不存在的。要尋找一個足夠好的計劃,足夠讓你啟動專案就行了。因為計劃總是趕不上變化,但是計劃可以推動你向有組織的方向進行,這會使你的程式碼更清晰。計劃太多不過是浪費時間而已。

我現在講的都只是對於小功能的計劃,想在一開始就計劃好所有功能的做法就更不可取!這在軟體工程中被叫做瀑布模型,是一種系統線性的計劃,每一個步驟都按順序完成。可以設想這種瀑布模型需要多少計劃啊。這不是這裡所討論的計劃型別。瀑布模型在大多數軟體工程中都是無效的。任何複雜的事物都只能根據現實靈活調整來實現。

編寫程式必須是一個響應的過程。你會新增以前從未想過的新功能,這在瀑布模型中是無法想象的(譯註:因為瀑布模型把整個生命週期都計劃好了,不會有意外的功能),你也會因為從未想過的原因移除一些功能。你需要修復 bug 以適應變化。你需要靈活一些。

雖然如此(不能過度計劃),但一定要計劃一下後續的少數新功能。要很小心地計劃,因為不足或者過度的計劃都可能損害你程式碼的質量,可不能拿程式碼質量來冒風險。

3)低估程式碼質量的重要性

如果你只能夠關注你所寫的程式碼的一個方面,那麼肯定是可讀性。表意不明的程式碼就是垃圾,甚至是不可回收的垃圾。

永遠都不要低估程式碼質量的重要性。把編寫程式碼看作是一種溝通實現的方式。作為程式設計師最主要的工作就是清晰地交流當前解決方案的實現。

關於程式設計,我最喜歡的名言之一是:

總是以這樣的心態寫程式碼:彷彿最終維護你程式碼的那個人是個變態暴力狂,他知道你住在哪裡。

— John Woods

Jonh, 真是個明智的建議!

即便是微小的細節也很重要。舉個例子,如果你在你的程式碼中,縮排和大小寫的風格不一致,你簡直就該被“吊銷程式設計執照”。

tHIS is
  WAY MORE important

than
         you think
複製程式碼

另一點是關於長行的使用。任何超過 80 個字元的程式碼行都要難讀很多。你可能試圖把一些很長的條件判斷放在同一行,好讓 if 語句更清晰,別這麼做。永遠都不要讓一行程式碼超過 80 個字元,永遠都不要。

很多這樣的簡單問題都可以通過 lintingformatting 工具修復。在 JavaScript 中,有兩個非常棒的工具可以完美協作:ESLintPrettier。給自己行行好,把它們用起來吧。

以下還有一些和程式碼質量相關的誤區:

  • 在一個方法或一個檔案中寫非常多行程式碼。你應該總是把很長的程式碼拆分成小的片段,以便測試和單獨管理。我個人認為超過 10 行的方法都算太長了,但這只是個經驗之談。

  • 使用雙重否定。拜託,請別不要不這麼做(譯註:此處故意使用雙重否定╮(╯▽╰)╭)。

使用雙重否定也並不是不會錯(譯註:此處作者故意使用雙重否定)

  • 使用短而通用、或基於型別的變數名。給你的變數一個描述性和沒有歧義的名字。

電腦科學中只有兩件難事:清除快取和命名。

— Phil Karlton

  • 毫無描述地硬編碼字串字面量和數字字面量(譯註:即“魔數”)。如果你需要寫一些依賴固定字面量字串值或數字值的程式碼,那就使用常量儲存這些固定值,並起一個好變數名。
const answerToLifeTheUniverseAndEverything = 42;
複製程式碼
  • 使用邋遢的捷徑和奇技淫巧避免在簡單問題上花費更多時間。不要與問題共舞,直面現實吧。

  • 覺得程式碼越長越好。然而在大多數情況下,程式碼是越短越好。只有以程式碼可讀性更強為前提,才用長程式碼的寫法。舉個例子,不要內嵌大量三目運算子(?:)來讓程式碼變短。當然,也不要有意讓程式碼變得沒必要的冗長。刪除沒必要的程式碼在任何專案中都是你能做的最好的事。

使用程式碼行數來衡量程式設計進度就像使用重量來衡量飛行器的建造進度一樣。

— Bill Gates

  • 過度使用條件邏輯。你覺得需要條件邏輯的大多數情況都可以不使用條件邏輯來實現。考慮所有的代替方案,基於可讀性,僅僅選擇其中的一種。除非你已經可以測量效能了,否則不要優化效能。相關:避免 Yoda conditions 和根據條件賦值。

4)使用初次方案

還記得在我剛開始程式設計的時候,當我遇到一個問題時,我能找到一個解決方案然後馬上就投入這個方案中,我急忙忙地實現它,沒有考慮過這個初次方案複雜度和潛在的失敗情況。

雖然初次方案可能很誘人,但是好的方案卻往往是在你開始盤查多種方案時才發現的。如果你沒辦法找出這個問題的多種解決方案,那很可能是你沒有完全明白這個問題。

作為專業的程式設計師,你的職責不是找出問題的一個解決方案,而是找出問題的最簡單的解決方案。我所說的“簡單的解決方案”是指這個解決方案必須是準確的,效能足夠好,還要簡單易讀、易理解和易維護。

有兩種方式來構建軟體設計。一種是把它做得足夠簡單以至於明顯沒有缺陷,另一種是把它做得足夠複雜以至於沒有明顯的缺陷。

— C.A.R. Hoare

5)不放棄

另一個我經常犯的錯誤是我堅持我的初次解決方案,即使我已經確認了這可能不是最簡單的解決方案。這可能就是心理學上所說的“不放棄”心態吧。這在大多數活動中是一種好的心態,但在程式設計中卻不適用。事實上,當說到程式設計的時候,正確的心態應該是儘快失敗和多多失敗

當你開始懷疑一個解決方案的時候,你就應該考慮拋棄它,並且重新思考這個問題。不管你已經在這個解決方案中投入了多少精力。像 GIT 這樣的版本控制系統能夠幫助你分開管理和嘗試多種不同的解決方案,把它利用起來吧。

不要因為你在程式碼中花費了很多精力就為它著了魔。壞的程式碼就應該被丟棄。

6)不穀歌(譯註:不使用搜尋引擎)

很多時候我花費了大量寶貴的時間去嘗試解決一個問題,但其實我在一開始時只要簡單調研一下(譯者注:使用搜尋引擎)就能得到結果,這樣的例子數不勝數。

除非你正在使用一種極其前沿的技術,否則當你遇到一個問題時,很可能別人早就遇到過同樣的問題了,並且也找到了解決方案了。給自己省點時間,先 Google 一下

有時候,Google 一下可能會披露這樣一個事實:你覺得這是個問題但其實那並不是,你需要做的並非修復它,而是擁抱它。也不要覺得你知道了尋找解決方案必備的所有知識,Google 會讓你吃驚。

儘管如此,你在 Google 搜尋時需要小心。一個新手的標誌就是在沒有理解的情況下就複製貼上別人的程式碼,儘管這些程式碼可能正確地解決了你的問題,但你永遠都不應該使用你沒有完全理解的程式碼,哪怕只有一行。

如果你想成為一個有創造性的程式設計師,不要以為你知道自己在做什麼。

作為一個有創造力的人,最危險的想法就是以為你知道自己在做什麼。

— Bret Victor

7)沒有封裝

這一點不只是關於物件導向正規化的。使用封裝總是有用的。不使用封裝往往導致難以維護的系統。

在一個應用中,一個功能應該只在一個地方處理,這通常就是一個物件的職責,這個物件應該只向其他需要用到它的物件暴露必須的介面。這無關乎機密,而是為了降低一個應用中的不同部分之間的相互依賴。堅持這些法則能夠讓你安全地改變你的類、物件、函式的內部實現,而不用擔心破壞了修改之處外更大範圍的東西。

概念的邏輯單元和狀態應該有他們自己的。我說的類是指一個藍圖模版,這可能確實是一個物件,也可能是一個函式物件,你也可以認為是一個模組或一個

在一個邏輯單元以內,自包含的任務塊應該有他們自己的方法,一個方法應該只做一件事,並把這件事做好。相似的類應該使用相同的方法名。

作為一個初級程式設計師,我以前經常都無法自然地寫一個新的類來組織概念性的單元,也經常無法辨識什麼才是自包含的。如果你看到了一個“Util“工具類,就像一個垃圾場,堆放了很多互不關聯的程式碼,這就是新手程式碼的特點。如果你做了個微小的改動,發現這個改動有連鎖反應,需要改動很多其他地方,那就是新手程式碼的另一個特點。

在往一個類新增一個方法或者向一個方法新增更多職責的時候,思考一下,並且問問你的直覺。這裡你需要花費點時間,不要跳過或者想著“我稍後再來重構”,剛開始的時候就要做。

基本的想法就是你想你的程式碼高內聚低耦合,這只是個時髦一點的術語,意思是說保持相關的程式碼在一起(在一個類中),降低不同類之間的相互依賴。

8)為未知做計劃

經常會傾向於去考慮超出當前正在寫的解決方案的問題。每寫一行程式碼就有各種各樣的“萬一”在你腦海浮現。這在測試邊界情況的時候是很好的習慣,但如果將它作為潛在需求的驅動,就大錯特錯了。

你需要辨識你的這些“萬一”屬於上面說的兩種型別中的哪一類。不要編寫你現在不需要的程式碼。不要為未知的將來作計劃。

因為你覺得可能以後會使用到一個功能,就去編寫程式碼實現它,這是很顯然的錯誤。不要這樣做。

儘可能編寫目前正在實現的方案所需的最少量程式碼。當然,要處理邊界情況,但不要新增邊界功能

為了增長而增長是癌細胞的思想。

— Edward Abbey

9)沒有使用合適的資料結構

初級程式設計師在準備面試的時候通常會太過關注演算法。能夠辨識好的演算法並在需要的時候使用是很好的事。但記憶這些演算法可不會給你的程式設計技能帶來提升。

不同的是,記憶你所用的程式語言中的各種資料結構的優缺點肯定能使你成為更好的開發者。

使用錯誤的資料結構是一個巨大和明顯的廣告牌,上面寫著“這是新手的程式碼“。

本文並不是要教你資料結構方面的知識,但這裡快速舉幾個例子:

- 使用列表(陣列)而不是對映表(物件)來管理記錄

最經常犯的資料結構方面的錯誤就是使用列表而不是對映表來管理一系列物件。沒錯,你應該使用對映表來管理一個記錄列表

要注意的是我這裡討論的記錄列表是其中的每一項記錄都有一個可以用於查詢物件的唯一標識。使用列表來管理標量值是可以的,並且通常也是更好的選擇,特別在重點用法是“壓入”一些值到列表的情況下。

在 JavaScript 中,最常用的列表結構是陣列,最常用的對映表結構是物件(在現代 JavaScript 中也有對映表的結構)。

使用列表而不使用對映表來管理物件通常都是錯的。儘管這個說法確實是在管理大量記錄的時候才成立,而我想說堅持一直這麼做吧。這麼做很重要的主要原因就是使用唯一標識來查詢物件的時候,對映表比列表要快得多。

- 沒有使用堆疊

當編寫一些需要遞迴形式的程式碼的時候,通常很容易使用簡單的遞迴函式。然而,優化遞迴程式碼通常很難,特別是在單執行緒環境下。

優化遞迴程式碼取決於遞迴函式返回了什麼。比如說,優化一個返回撥用自身兩次以上的遞迴函式比優化只返回撥用自身一次的函式要難得多。

作為初學者我們通常會忽視的就是其實有遞迴函式的替代方法。你可以使用資料結構。手動把函式呼叫結果 Push 入棧,然後在需要獲取結果的時候把結果 Pop 出棧。

10)把既有程式碼弄得更糟

假設給你一個像這樣的凌亂的屋子:

[譯] 我在程式設計初級階段常犯的錯誤

現在要求你在這個房間裡面放置一件東西。由於房間現在已經是很混亂了,你很可能把東西隨便一放,幾秒鐘就完事了。

當你面對的是混亂的程式碼的時候,千萬別這麼做。不要把程式碼弄得更亂!總是要讓程式碼比你剛接手的時候乾淨那麼一點。

以上房間問題中你應該做的是清理需要的部分,來給新的東西騰出合適的位置。比如說,如果這件東西是一件衣服,需要被放到衣櫃裡,那麼你就需要清理出一條到衣櫃的路出來。這是正確完成這個任務的一部分。

以下有一些錯誤的實踐,通常會使得程式碼比以前更糟糕(不完備的列表):

  • 複製程式碼。如果你複製/貼上一份程式碼之後只改了一行,你簡直就是在產生重複程式碼並且把程式碼弄得更糟。放到以上凌亂房間的例子中就是,你拿進了一張更低基座的椅子而不是考慮使用一張可調節高度的椅子。總是要在心裡想著抽象的概念,一旦可以就要使用它。
  • 沒有使用配置檔案。如果你要使用一個在其他環境下可能不一樣的值,或者在其他時間可能不一樣的值,這個值就應該放在配置檔案中。如果你需要在你程式碼的不同地方都使用一個值,這個值也應該放置在配置檔案中。當你要引入一個新的值到你的程式碼中的時候,只需要問問你自己:這個值應不應該放在配置檔案中?答案很可能是“應該”。
  • 使用沒必要的條件語句和臨時變數。每一個 if 語句都是一個需要測試兩次的邏輯分支。當你可以在不犧牲可讀性的情況下避免條件語句的時候,你就應該這麼做。這裡主要的問題在於使用分支邏輯擴充套件一個函式還是引入另一個函式。每一次你覺得需要 if 語句或一個新的函式變數的時候,你應該問問自己:我是否在正確的層次修改程式碼,還是說我應該在更高的層次考慮一下這個問題。

關於不必要的 if 語句,看一下以下的程式碼:

function isOdd(number) {
  if (number % 2 === 1) {
    return true;
  } else {
    return false;
  }
}
複製程式碼

以上的 isOdd 函式有幾個問題,但你能看出最明顯的一個嗎?

他使用了不必要的 if 語句,以下是一種等價的寫法:

function isOdd(number) {
  return (number % 2 === 1);
};
複製程式碼

11)給顯而易見的程式碼寫註釋

如今我已經學會了如何盡我所能去避免在寫註釋時面臨的難題了。大多數的註釋都可以使用更好命名的元素(譯註:類、方法、變數)替換。

舉個例子,不要寫下面這樣的程式碼:

// This function sums only odd numbers in an array
const sum = (val) => {
  return val.reduce((a, b) => {
    if (b % 2 === 1) { // If the current number is even
      a+=b;            // Add current number to accumulator
    }

    return a;          // The accumulator
  }, 0);
};
複製程式碼

同樣的程式碼可以不需要註釋,像這樣重寫:

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (isOdd(currentNumber)) { 
      return accumulator + currentNumber;
    }

    return accumulator;
  }, 0);
};
複製程式碼

僅僅是給函式和引數更好的命名就可以省去大部分的註釋,在寫註釋前請記住這一點。

然而,有時候你可能被迫進入這樣的情況,只有通過增加註釋才能提高程式碼的清晰性。這時候你就應該組織你的註釋來回答為什麼是用這段程式碼而不是這段程式碼是幹嘛的

如果你強烈地想要寫一段註釋來解釋“這段程式碼是幹什麼的”,以增加程式碼清晰性,請不要寫那些顯而易見的。以下是一個例子,其中無用的註釋只會徒增程式碼的干擾性。

// 建立一個變數並初始化為 0
let sum = 0;

// 遍歷陣列
array.forEach(
  // 對於陣列中的每一個數字
  (number) => {
    // 把當前數字加到變數 sum 中
    sum += number;
  }
);
複製程式碼

別做上面那樣的程式設計師。也不要接受這樣的程式碼。不得不處理的情況下,刪了那些註釋。如果你碰巧僱用了寫出上面那樣註釋的程式設計師,炒了他,馬上炒。

12)沒有寫測試

我將簡單地闡述這一點,如果你覺得你是個程式設計師專家並且這樣的想法給你寫程式碼不帶測試的自信,在我的字典裡你就是個新手。

如果你不在程式碼裡寫測試的話,那麼你很可能在用某些手動的方式測試你的程式碼。如果你在構建一個網頁應用的話,每次修改幾行程式碼你就得在瀏覽器中重新整理然後做一些互動來再次測試。我也是這麼做的。手動測試程式碼沒有錯,但你應該通過手動測試來弄清楚如何進行自動化測試。如果你在你的應用中測試了一個互動功能,那麼在你新增更多功能程式碼之前,你就應該先把這個互動功能的測試用程式碼自動化。

你是一個人,你就很難保證在每次修改完程式碼之後還能把之前所做的所有測試校驗都再做一遍,那就讓計算機幫你做吧。

如果可能的話,甚至在開始寫程式碼實現需求之前,你就應該開始預估和設計需要測試校驗的情況了。測試驅動開發 (Testing-driven development, TDD)可不是什麼花俏的炒作,它是會實實在在會對你思考功能特性、尋找更好的設計方案產生積極影響的。

測試驅動開發(TDD)並非對每一個人和對每一個專案都奏效的,但如果你能夠把它利用起來(哪怕只在專案的某一部分),你都完全應該這樣去做。

13)覺得程式碼執行起來了就是能正確執行

看一下這個實現“把所有奇數相加”功能的函式 sumOddValues,有什麼問題嗎?

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (currentNumber % 2 === 1) { 
      return accumulator + currentNumber;
    }

    return accumulator;
  });
};
 
 
console.assert(
  sumOddValues([1, 2, 3, 4, 5]) === 9
);
複製程式碼

測試斷言通過了,生活真美好啊,真的,真的對了嗎?

問題在於以上的程式碼是不完備的,在少數情況下它能正確處理,碰巧測試使用的斷言剛好就是這些情況中的一種,但除此之外還有很多問題,讓我們列舉其中的幾個:

- 問題一: 沒有處理空輸入。如果這個函式被呼叫的時候沒有傳遞任何引數呢?這種情況下就會產生一個錯誤,暴露了這個函式的內部實現。

TypeError: Cannot read property 'reduce' of undefined.
複製程式碼

[譯] 我在程式設計初級階段常犯的錯誤

這通常是糟糕程式碼的一個標誌,理由如下:

  • 函式的使用者不應該看到函式的具體實現。
  • 出錯的資訊對使用者沒有任何幫助,函式不起作用就是不起作用。但是,如果函式的出錯資訊能對函式的使用方法描述得更清晰具體一點,函式的使用者就可能知道了是他們使用姿勢不當。比如你可以選擇讓這個函式丟擲自定義的異常,像下面這樣:
TypeError: Cannot execute function for empty list.
複製程式碼

除了丟擲一個異常,你也可以重新設計這個函式,忽略掉空的輸入,然後返回 0。不管怎麼樣,在這種情況下你都應該做些處理。

- 問題二: 沒有處理異常輸入。如果呼叫函式的時候沒有傳遞一個陣列,而是傳入了一個字串,一個整數,或者一個物件,此時會發生什麼?

現在這個函式就會丟擲這樣的錯誤了:

sumOddValues(42);

TypeError: array.reduce is not a function //(譯註:array.reduce 不是一個函式)
複製程式碼

好吧,真是不幸,因為 array.reduce 絕對是一個函式啊!

由於我們給函式的引數命名為 array,任何呼叫這個函式的引數(即以上例子中的 42)在函式內部都會打上 array 的標籤,這個錯誤其實就是說 42.reduce 不是一個函式。

你親眼看到了這樣的錯誤多麼令人費解,不是嗎?也許更有幫助的錯誤是這樣的:

TypeError: 42 is not an array, dude. // (譯註:42 可不是陣列啊,我的大胸弟。)
複製程式碼

問題一和問題二有時候被稱為邊界情況,有一些基本的邊界情況要考慮,但是也經常有一些不那麼明顯的邊界情況,也需要考慮進來。比如說,如果我們傳遞了負數作為引數呢?

sumOddValues([1, 2, 3, 4, 5, -13]) // => 還是 9
複製程式碼

呃,-13 也是一個奇數。這是你預期的行為嗎?是不是應該丟擲異常?負數是不是也應該被求和加起來?還是說像它現在的行為這樣,僅僅忽略掉負數?現在你可能會意識到這個函式的名稱本來應該被叫做 sumPositiveOddNumbers (對正奇數進行求和)。

這種情況下做決策很容易,但更重要的一點是,如果你不寫測試用例記錄你這次決策的原因,這個函式將來的維護者可能會不知道你是有意忽略掉負數還是這裡有 bug,並對此毫無頭緒。

這不是 bug ,這是特性。

— 忘了寫測試程式碼的某某

- 問題三: 並非所有的有效情況都被測試了。拋開邊界情況不說,這個函式還有一種合法的、非常簡單的情況沒有被正確處理:

sumOddValues([2, 1, 3, 4, 5]) // => 11
複製程式碼

以上的 2 也被加到求和結果裡面去了,但這本不應該。

答案很簡單,reduce 接受另一個引數,作為求和器的初始值。如果這個引數沒有傳遞的話(如上程式碼),reduce 函式就會用集合的第一個值作為求和器的初始值。這就是以上例子中第一個偶數也會被求和加起來的原因。

儘管你可能在你一開始寫程式碼的時候就馬上意識到這個問題了,但是暴露出這個問題的測試用例還是應該首先被包含到測試集中,跟其他很多基本測試用例一起,如“傳遞全是偶數的陣列”,“傳遞包含 0 的陣列“,還有”傳遞空陣列“。

如果你看到了很少量的測試用例,還沒有處理大多數甚至根本不處理邊界情況,那就是新手程式碼的另一個標誌。

14)沒有質疑既有程式碼

除非你是個一直單飛的超級碼農,否則毫無疑問,你在生活中肯定會遇到一些很傻逼的程式碼。初學者通常無法辨別這些程式碼的好壞,並且通常會覺得這些程式碼是好程式碼,因為這些程式碼似乎正常執行,還在程式碼倉庫裡面待了很長時間。

更糟的是,如果糟糕的程式碼用了一些糟糕的實踐,初學者很可能就在程式碼倉庫裡的其他地方應用了這些糟糕的實踐,因為他們把這些當作好程式碼給學習過來了。

有一些程式碼看起來很糟糕,但可能是有一些特殊的情況迫使開發者寫出這樣的程式碼,這就是應該編寫詳細註釋的地方了,可以在這裡告訴初學者這些特殊的情況以及程式碼這樣寫的原因。

作為一個初學者,總是應該假定那些你讀不懂的、且沒有文件註釋的程式碼很可能就是糟糕的程式碼。質疑之,詢問之,使用 git blame 揪出罪魁禍首!

如果程式碼作者已經離開很久了,或者他自己也記不起來了,那就好好研究這些程式碼,盡力弄懂相關的一切。只有當你完全理解了這份程式碼,你才能夠建立起這份程式碼好壞的認知,在那之前不要做任何假設。

15)迷戀最佳實踐

我覺得“最佳實踐”其實是害人的,它暗示著你不需要深入研究它,這就是有史以來最佳實踐,不用質疑!

沒有最佳實踐這回事,也許有目前來說針對這門語言的好的實踐。

一些以前被認為是程式設計中最佳實踐的,現在卻被貼上了最差實踐的標籤。

如果你投入足夠多的時間,你總是可以找到更好的實踐。不要擔心什麼最佳實踐,專注於你能做到最好的地方。

不要因為你在某些地方讀到過,說可以這麼做你就去這麼做,也不要因為你看過別人這麼做你也去做,也不要因為別人說這是最佳實踐你就這麼做,包括本文中給出的所有建議!質疑一切,挑戰權威,瞭解所有可能的選擇,作出明智的決定。

16)迷戀效能

過早優化是萬惡之源

— Donald Knuth (1974)

儘管自 Donald Knuth 寫下上面的言論以來,計算機程式設計已經發生了翻天覆地的變化,我覺得這句話在今天仍然是很有價值的建議。

有一條法則可以幫助記憶這一點:如果你無法測量程式碼中疑似存在的效能問題,那就不要試圖去優化它。

如果你在執行程式碼之前就在優化它了,那很可能你就是在過早優化程式碼了,也很可能你正在費時費力做的優化是完全沒必要的。

當然了,有一些很明顯的優化是你必須在引入新程式碼前就要考慮的。比如說在 Node.js 中,不要讓事件氾濫成災或阻塞呼叫棧,這是至關重要的。這是你應該始終牢記的早期優化的一個例子。捫心自問:我正在考慮的這部分程式碼會阻塞呼叫棧嗎?

在未經測量的現有程式碼中進行任何不明顯的優化都是有害的,也應該儘量避免。你可能覺得完成之後是一種效能收益,然而結果卻可能是新的、意料之外的 bug 的源頭。

不要浪費時間去優化未經測量的效能問題。

17)沒有以終端使用者體驗為目標

給應用新增一個功能最簡單的方法是什麼?從你自己的視角來看這個功能,或者看新功能如何融入到目前的使用者介面中,對吧?如果新功能是要從使用者那裡獲取一些輸入,那就在你已有的表單上新增,如果新功能是要在頁面上新增一個連結,那就在你已有的選單列表裡面新增。

別做那樣的開發者。 要做專業的開發者,站在終端使用者的角度看問題。專業的開發者要考慮這個特定功能的使用者需要什麼、怎樣使用,要想方設法使得這個功能容易讓使用者發現和使用,而不是想方設法在應用中用最便捷新增這個功能,毫不考慮這個功能的可發現性和可用性。

18)沒有為任務挑選合適的工具

每一人都有一個最喜愛工具的列表,在他們的程式設計相關的活動中起到輔助的作用。一些工具很優秀,也有一些工具很辣雞,但是大部分工具都很擅長處理某一特定的任務,對除此之外的任務就不那麼在行了。

錘子是把釘子釘進牆壁裡的絕妙工具,但是用來擰螺絲就是最差的工具了。不要因為你很喜愛那個錘子你就用它來擰螺絲。不要因為這是個很流行的錘子、在亞馬遜上使用者評分有 5.0 分就用它來擰螺絲。

依賴一個工具的流行度而不是它對一個問題的適用性來選擇工具是真正新手的標誌。

關於這點有一個問題是,你可能不知道適用某個特定任務的“更好的”的工具。在你目前的認知裡,這個工具可能就是你所知道的最好的。但是,如果跟其他可選工具做比較的話,它就不是首選了。你需要使自己瞭解這些可選的工具,對這些新工具保持開放的心態,以後你可能會使用到它們。

一些程式設計師不願意使用新的工具。他們對於目前正在使用的工具很滿意,也不想學習新的工具。我理解這種做法,但這種做法很明顯是錯的。

你可以使用最原始的工具建造房子,然後享受甜蜜時光。你也可以花費一些時間和金錢去了解先進的工具、更快地建造更好的房子。工具在不斷地改進中,你要樂意去學習它們、使用它們。

19)不理解程式碼問題會導致資料問題

程式的一個重要方面通常是管理某些形式的資料。程式就是一個新增新資料、刪除舊資料、修改已有資料的介面。

即使是程式中最小的 bug 也會導致它所管理的資料去到一種不可預測的狀態。尤其是當所有資料校驗都完全在這個有 bug 的程式中進行時。

當涉及到“程式碼-資料”關係時,初學者可能無法馬上理解其中的聯絡。他們可能覺得在生產環境中繼續使用這段有 bug 的程式碼沒什麼大不了的,因為失效的功能 X 也不是超級重要。問題在於這段有 bug 的程式碼會持續產生資料完整性問題,這在一開始可不那麼明顯。

更糟的是,如果部署了 bugfix 的程式碼而沒有修復相應 bug 導致的細微資料問題,也會讓更多的資料問題累積,最終“積重難返”。

你怎麼做才能使自己免受這些問題的困擾?你可以簡單地使用多層資料完整性驗證。不要依賴於單一的使用者介面。在前端、後臺、網路層、資料層都進行資料驗證。如果做不到這樣,那至少要在資料庫層做約束。

要熟悉資料庫的約束,在你往資料庫裡新增表、列的時候儘可能的用上這些約束:

  • NOT NULL 非空約束表示空值 NULL 無法被儲存到該列上。如果你的應用假定了這個列的值是存在的,那就應該在資料庫裡把這個列定義為非空。
  • UNIQUE 唯一約束表示在整個資料表中,該列上的所有值都不能出現重複。舉個例子,這在使用者表中的使用者名稱欄位和郵件欄位是極好的使用場景。
  • CHECK 約束是一個自定義的表示式,想要被資料庫接受的資料都必須使該表示式計算結果為 true。舉個例子,如果有一個常規的百分比列,它的值介於 0 到 100 之間,就可以使用 CHECK 約束來確保這一點。
  • PRIMARY KEY 主鍵約束表示這個列的值必須同時是非空和唯一的。你目前可能就已經在用了。資料庫的每一個表都應該有一個主鍵以唯一標識一條記錄。
  • FOREIGN KEY 外來鍵約束表示該列的值必須和另一個表的一個列(通常是主鍵)的值匹配。

新手關於資料完整性的另一個問題是缺乏事務的觀念。如果改變同個資料來源的多個操作彼此依賴,它們就必須被包含在同一事務中,以便在其中一個操作失敗時能夠回滾。

20)重複造輪子

This is a tricky point. In programming, some wheels are simply worth reinventing. Programming is not a well-defined domain. So many things change so fast and new requirements are introduced faster than any team can handle. 這是很棘手的一點。在程式設計領域,有些輪子確實值得重新造一遍。程式設計不是一個明確定義的領域。如此多的事物、變化如此之快、新需求的引進也如此之快,沒有一個團隊能夠完美處理這些。

比如說,如果你需要一個輪子,根據一天的不同時間以不同的速度旋轉,也許我們就不是去考慮改造那些我們所知的、所愛的輪子了,而要考慮重新造一個。但是,除非你確實需要一個非常規設計的輪子,否則都不要重新造一個新的輪子,就用那個該死的輪子吧。

有時候在眾多可選的牌子裡選擇一個所需的輪子是很具挑戰性的。在購買之前要先調研和使用!關於軟體“輪子”很酷的一點是,大部分的輪子都是免費的、開放的,你可以看到它們的內部設計。你能夠輕而易舉地通過判斷它們的內部設計的質量來判斷軟體輪子的好壞。如果可能,使用開源的輪子。開源的軟體包可以很容易的除錯和修復。也可以很容易的替換掉。此外,你還可以足不出戶地支援它們。

不過,如果你需要一個輪子,千萬不要去買一整輛車,然後把你正在維護的那輛車放到新買的車頂部。不要僅僅為了使用一兩個函式就引入一整個程式碼庫,在 JavaScript 中的典型例子就是 lodash 程式碼庫。如果你要隨即打亂一個陣列,只要引入 shuffle 方法就好了。不要引入整個的 loadash 程式碼庫,很可怕。

21)對程式碼複審的錯誤態度

程式設計師新手的一個標誌就是他們經常把程式碼複審看作是批評。他們不喜歡程式碼複審、不感激程式碼複審、甚至恐懼程式碼複審。

這是錯誤的。如果你也這麼覺得,你需要馬上就改變這種態度。把每一次程式碼複審當作是學習的機會,歡迎他們、感激他們、從中學習,最重要的,當你從你的程式碼複審人員那裡學習到東西的時候,要感謝他們。

在程式設計道路上,你永遠都是個學習者。承認這一點。大多數的程式碼複審都能夠讓你學到以前不知道的一些東西。把它們當作學習的資源。

有時候程式碼稽核人員也會犯錯,那就輪到你來教他們了。但是如果無法僅僅從你的程式碼中就明顯看出問題,也許在那種情況下你的程式碼就需要相應的修改了。如果你無論如何都需要給你的複審人員上一課的話,那就記得,對程式設計師來講,教會別人是最有利的活動之一。

22)沒有使用版本控制系統

新手往往低估了一個好的版本控制系統的威力,我這裡所說的好的版本控制系統其實就是指 Git

原始碼控制並不僅僅是你把你的更改推送給別人,讓他們在此之上接著開發,除此之外還有更重要的。原始碼控制是和清晰的歷史相關的。原始碼會被質疑,程式碼的歷史程式能夠輔助回答這些困難的質疑。這就是為什麼我們如此關注提交資訊的原因。它們還是又一種交流實現的通道,使用提交資訊能夠幫助將來的維護者弄清程式碼為什麼會發展到目前的狀態。

應該經常提交和儘早提交。為了保持一致性,請在提交資訊主題行(譯註:Git 提交資訊的第一行)使用(譯註:英文)動詞的一般現在時形式。需要時刻注意,提交的資訊要儘可能詳細,但也要儘可能是總結性的。如果你需要多幾行來寫提交資訊,那很可能就是你的提交包含太多了,Rebase!

不要在提交資訊中包含任何無關的東西。比如說,不要列出你新增、修改、刪除了哪些檔案,這些在提交物件的本身就包含了,並且可以輕易地使用一些 Git 命令引數就顯示出來,在提交資訊中包含這些顯然就是混淆視聽。有些團隊喜歡為每一個更改的檔案寫一個總結資訊,在我看來那是“提交資訊太多”的標誌。

原始碼控制也是和可發現性相關的。如果你看到一個函式,並且開始質疑它的必要性和設計,你可以找到引入這個函式的提交記錄,看到這個函式的上下文。提交記錄甚至可以幫助你認清哪些程式碼帶來了 bug。Git 甚至提供了在各個提交之間進行二分查詢,幫助定位到引入 bug 的提交的工具(bisect 命令)。

原始碼控制也可以被利用在一些神奇的地方,甚至在更改還沒進入官方提交記錄的時候。諸如暫存區變更、選擇性打補丁、重置、暫存、修改、應用、比較、撤回等等工具為你的工作流提供了豐富的工具。瞭解它們、學習它們、使用它們、感激它們。

在我的字典裡,Git 功能你懂得越少,你就越像一個新手。

23)過度使用共享狀態

再次宣告,這不是關於函數語言程式設計和其他正規化的討論,那是另一篇文章的主題了。

事實就是,共享狀態是出現問題的來源之一,如果可能的話,應該避免使用共享狀態,如果避免不了,就應該最低限度地使用共享狀態。

在我還是初級程式設計師的時候,我經常沒有意識到,我定義的每一個變數都代表了一種共享狀態,它所持有的資料能夠被同一作用域內的所有元素修改。一個共享狀態的作用域越大,它的跨度就越長。儘量讓新的狀態包含在小的作用域內,並且保證它們不會逸出。

關於共享狀態的大問題是,在同一個 event loop(對於基於 event-loop 的環境) 中,當有多個資源需要同時修改這個狀態的時候,就會出錯。這時會發生競態條件。

重點來了:一個新手可能會嘗試使用定時器來解決這個共享變數的競態條件問題,特別是當他們必須處理一個資料鎖的問題時。這是危險的標誌,別這麼做,注意它,在程式碼複審中指出它,永遠也不要接受這樣的程式碼。

24)面對 Error 時的錯誤態度

Error 是好東西。這意味著你在進步,意味著你可以通過簡單的後續修改就獲得更多的進步。

專業程式設計師喜愛 Error。新手則痛恨 Error。

如果看到這些驚豔的紅色 Error 資訊讓你很困擾,你就需要改變這種態度了。你需要把它們看作助手。你需要處理它們,需要利用它們來進步。

有一些 Error 需要被改進為異常(Exception)。異常是使用者定義的 Error,這些 Error 是你在計劃之內的。有些 Error 則不需要管,它們就應該使程式奔潰並退出。

25)沒有休息

你是一個人,你的頭腦就需要休息。你的身體也需要休息。你經常很在狀態以致於忘了休息。我把這視作新手的另一個標誌。這不是你可以妥協的事情。在你的工作流中整合一些東西來迫使你中途休息。中途短暫休息很多次,離開你的椅子,走一小段路,以此來想清楚接下來應該做什麼,稍後雙眼清晰地回來繼續寫程式碼。

這真是篇長文章。你應該休息一下了。

感謝閱讀


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章