[譯] 如何編寫整潔代碼?從 Robert C. Martin 的“代碼整潔之道”中吸取的教訓

如何編寫整潔代碼?從 Robert C. Martin 的“代碼整潔之道”中吸取的教訓

有兩件事 —— 編程和良好的編程。編程是我們一直在做的事情。現在是時候關注良好的編程了。我們都知道,即使是糟糕的代碼也能工作。但是寫好代碼,需要花費時間和資源。此外,當其他開發者試圖找出你代碼的運行細節,他們會嘲笑你。但是,關心你的程序永遠不會太遲。

這本書給了我很多關於最佳實踐和如何編寫代碼的知識。現在,我爲自己的編程技能感到羞愧。儘管我總是努力改善我的代碼,但是這本書教會我的更多。

現在,你閱讀這篇博客有兩種原因。第一,你是個程序員;第二,你想成爲更好的程序員。很好,我們需要更好的程序員。

整潔代碼的特徵

  1. 應該是優雅的 —— 整潔的代碼讀起來令人愉悅。讀這種代碼,就像見到手工精美的音樂盒或者設計精良的汽車一般,讓你會心一笑。
  2. 整潔的代碼力求集中。每個函數、每個類和每個模塊都全神貫注於一件事,完全不受四周細節的干擾和污染。
  3. 整潔的代碼是有意維護的 —— 有人曾花時間讓它保持簡單有序。他們適當地關注到了細節。他們在意過。
  4. 能通過所有的測試
  5. 沒有重複代碼
  6. 包括儘量少的實體,比如類、方法、函數等

如何編寫整潔代碼?

有意義的命名

名副其實。選個好名字要花時間,但省下來的時間要比花掉的多。變量、函數或類的名稱應該已經回覆了所有的大問題。它該告訴你,它爲什麼會存在,它做什麼事,它應該怎麼用。如果名稱需要註釋來補充,那就不算是名副其實。

Eg- int d; // 消逝的時間,以日計

我們應該選擇指明瞭計量對象和計量單位的名稱。

更好的命名應該是:-int elapsedTime。(儘管書中說的是 elapsedTimeInDays,但是我仍然傾向於前者。假設運行的時間改爲毫秒,我們不得不將 int 改爲 long,並且用 elapsedTimeInMillis 替換 elapsedTimeInDays。我們不知道何時是個盡頭。)

類名 —— 類名和對象名應該是名詞或名詞短語,如 Customer、WikiPage、Account 和 AddressParser。避免使用 Manager、Processor、Data 或 Info 這樣的類名。類名不應當是動詞。

方法名 —— 方法名應當是動詞或動詞短語,如 postPayment、deletePage 或者 save。屬性訪問器、修改器和斷言應該根據其值命名,並加上 get、set 前綴。

重載構造器時,使用描述了參數的靜態工廠方法名。例如,

Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好於
Complex fulcrumPoint = new Complex(23.0);

每個概念對應一個詞 —— 每個抽象概念選一個詞,而且一以貫之。例如,使用 fetch、retrieve 和 get 來給多種類中的同種方法命名。你怎麼能記得住哪個類中對應的是哪個名字呢?同樣,在一堆代碼中有 controller,又有 manager,還有 driver,就會令人困惑。DeviceManager 和 Protocol-Controller 之間有何根本區別?

函數

函數的第一規則就是要短小,第二條規則是還要更短小。這意味着 if 語句、else 語句、while 語句等,其中的代碼塊應該只有一行。該行大抵應該是一個函數調用語句。這樣不僅能保持函數短小,而且,因爲塊內調用的函數擁有具體說明性的名稱,從而增加了文檔上的價值。

函數參數

一個函數不應該有超過 3 個參數,儘可能使其少點。一個函數需要兩個或者三個以上參數的時候,就說明這些參數應該封裝爲類了。通過創建參數對象,從而減少參數數量,看起來像是在作弊,但實則並非如此。

現在,當我說要減少函數大小的時候,你肯定在想如何減少 try-catch 的內容,因爲,它使你的代碼變得越來越臃腫。我的答案是隻生成一個僅包含 try-catch-finally 語句的方法。將 try/catch/finally 代碼塊從主體部分抽離出來,另外形成函數。

public void delete(Page page) {
  try {
     deletePageAndAllReferences(page);
  }
  catch (Exception e) {
    logError(e);
  }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
    logger.log(e.getMessage());
}

這使得邏輯變得清晰明瞭,函數名能更容易描述我們想要表達的。錯誤處理可以忽略。有了這樣美妙的區隔,代碼就更易於理解和修改了。

錯誤處理就是一件事 —— 函數應該只做一件事。錯誤處理就是一件事。如果關鍵字 try 在某個函數中存在,它就該是這個函數的第一個關鍵字,而且在 catch/finally 代碼塊後面也不該有其他內容。

註釋

如果你通過寫註釋來證明你的觀點,那你就大錯特錯了。理想情況下,根本不需要註釋。如果你的代碼需要註釋,說明你做錯了。我們的代碼應該闡述一切。現代編程語言是英語,我們能更加容易闡述自己的觀點。正確的命名能避免註釋。

與法律有關的註釋除外,它們是有必要的,與法律有關的註釋是指版權及著作權聲明。

對象和數據結構

這是個複雜的話題,所以要多加留意。首先,我們要澄清對象與數據結構之間的區別。

對象把數據隱藏於抽象之後,暴露操作數據的函數。數據結構暴露其數據,沒有提供有意義的函數。

這兩件事完全不同,一個是關於存儲 數據,另一個是關於如何操作這些數據。例如,考慮到過程式代碼形狀規範,Geometry 類操作三個形狀類。形狀類都是簡單的數據結構,沒有任何行爲。所有行爲都在 Geometry 類中。

public class Square {
  public Point topLeft;
  public double side;
}

public class Rectangle {
  public Point topLeft;
  public double height;
  public double width;
}

public class Circle {
  public Point center;
  public double radius;
}

public class Geometry {
  public final double PI = 3.141592653589793;
  public double area(Object shape) throws NoSuchShapeException {
    if (shape instanceof Square) {
        Square s = (Square)shape;
        return s.side * s.side;
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle)shape;
        return r.height * r.width;
    } else if (shape instanceof Circle) {
        Circle c = (Circle)shape;
        return PI * c.radius * c.radius;
    }
        throw new NoSuchShapeException();
    }
}

想想看,如果給 Geometry 類添加一個 perimeter() 函數會怎麼樣。那些形狀類根本不會因此而受影響!另一方面,如果添加一個新形狀,就得修改 Geometry 中的所有函數來處理它,再讀一遍代碼,注意,這兩種情形也是直接對立的。

現在考慮上述場景的另一種方法。

public class Square implements Shape {
  private Point topLeft;
  private double side;
  public double area() {
    return side*side;
  }
}

public class Rectangle implements Shape {
  private Point topLeft;
  private double height;
  private double width;
  public double area() {
    return height * width;
  }
}

public class Circle implements Shape {
  private Point center;
  private double radius;
  public final double PI = 3.141592653589793;
  public double area() {
    return PI * radius * radius;
  }
}

現在,與之前的案例相比,我們能很輕鬆的添加新形狀,即數據結構。而且,如果我們只需在一個 Shape 類中添加 perimeter() 方法,則我們就必須在所有 Shapes 類中實現該函數,因爲 Shapes 類是一個包含 area() 和 perimeter() 方法的接口。這意味着:

數據結構便於在不改動既有數據結構的前提下添加新函數。面向對象代碼(使用對象)便於在不改動既有函數的前提下添加新類。

反過來講也說得通:

過程式代碼(使用數據結構的代碼)難以添加新的數據結構,因爲必須修改所有的函數。面向對象代碼難以添加新函數,因爲必須修改所有的類。

因此,對於面向對象困難的事情對於面向過程來說很容易,對於面向對象容易的事情對於面向過程來說很困難。

在任何複雜的系統中,我們有時會希望能夠靈活的添加新的數據類型而不是新的函數。在這種情況下,對象和麪向對象就是最合適的。另外一些時候,我們希望能靈活的添加新函數而不是數據類型。在這種情況下,過程式代碼和數據結構將會更加合適。

老練的程序員知道,一切都是對象只是一個傳說。有時,你真的想要在簡單的數據結構上一些過程試的操作。因此,你必須仔細思考要實現什麼,也要考慮未來的前景,什麼是容易更新的。在這個例子中,因爲以後可能會添加其他新的形狀,所以我會選擇面向對象的方法。


我知道,在給定時間期限內完成你的工作,很難寫好代碼。但是你要耽擱多久呢?慢慢開始,堅持不懈。你的代碼可以爲你自己和其他人(主要受益者)創造奇蹟。我已經開始,並且發現了許多我一直在犯的錯誤。雖然它每天佔用我一些額外的時間,但將來我會得到報酬的。

這不是博客的結尾。我將繼續編寫關於代碼整潔之道的新方法。此外,我還將寫一些基礎的設計模式,這是從事任何技術的開發者都必須瞭解的知識。

同時,如果你喜歡我的博客並從中獲益,請鼓掌。它給了我更快創建新博客的動力 😃 歡迎進行評論/建議。不斷學習,不斷分享。

學習 Android APP 開發的完整指南

mindorks.com 上查看所有頂級教程

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即爲本文在 GitHub 上的 MarkDown 鏈接。


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章