iT邦幫忙

2021 iThome 鐵人賽

DAY 3
0

「我們是認真嚴肅地看待命名這件事,請您牢記這一點」

取自: Clean Code (p.20)

前言

  • 命名在軟體開發中無處不見,我們除了替:
    • 變數 (Variables)
    • 函式 (Functions)
    • 參數 (Arguments & Parameters)
    • 類別 (Class)
    • 套件 (Packages & Library) 命名
  • 也替
    • 原始檔 (Source Files)
    • 目錄 (Folders) 命名

我們替 jar、war、ear、json...等檔案不斷地命名再命名...


  • 這是一個專案的目錄結構:

    code/
    ├─ a/
    ├─ b/
    │  ├─ A
    │  ├─ B
    ├─ c/
    ├─ d.x/
    ├─ d.y/
    ├─ d.z/
    
  • 有可能從這樣的命名結構看出上列專案在做什麼嗎? 恐怕是很困難的
    實務上雖不至於有人會這麼胡亂地命名,但由於對命名 (Naming) 的小看,導致還沒點開程式碼,專案本身的可讀性就先下降了,這種情況倒是屢見不鮮...

  • 接著將上面的目錄結構改為:

    code/
    ├─ lib/
    ├─ machine/
    │  ├─ mipssim
    │  ├─ console
    ├─ network/
    ├─ build.cygwin/
    ├─ build.linux/
    ├─ build.macosx/
    
  • 我想每個讀者即使不了解作業系統,也能從架構中大概猜到這是一個:

    • 試圖模擬 MIPS 指令集的小型 Console 型作業系統
    • 有基本的 Library 與 Network 模組
    • Build 資料夾內是不同平台編譯後的檔案

好的命名使讀者在尚未深入細節前,就能對專案的模塊、類別的定位、函式行為...等有初步的概念


【補充】命名法

書中作者假設這是常識了,連提都不提 QQ
現代軟體開發基本都是採用下列命名慣例:

駝峰式(Camel Case) 命名法

小駝峰 (lowerCamelCase)

  • 開頭一定小寫,其餘單字首字母大寫
  • 常常用在區域變數(Local Variables)的宣告
  • 例如:
    string firstName = "JJJJ";
    string userPhone = "123456789"
    

大駝峰 (UpperCamelCase)

  • 又稱 Pascal case,跟小駝峰的差別在於首字母一定大寫
  • 很常套用在 類別(Class)方法(Method) 的命名
  • 例如:
    public class HomeController : Controller
    {
        public HomeController()
        {
        }
    
        public IActionResult Index()
        {
            return View();
        }
    }
    

蛇式 (snake_case)

  • 所有單字間的變換都以下底線來連接,又分為小蛇式與大蛇式
  • 以筆者經驗來說,Python 滿常使用 lower snake case 的命名方式
  • 例如:
    # 切分訓練資料集
    array = data.values
    X = array[:, :-1] 
    Y = array[:, -1].astype('int')
    x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
    

CH2: 有意義的命名(Naming)

讓名稱代表意圖 (使之名符其實)

  • 例如:

    int d; // elapsed time in days
    
    // vs.
    
    int elapsedTimeInDays;
    int daysSinceCreation;
    int fileAgeInDays;
    
  • 哪一種命名更有表達力呢?
    若這些變數是從外部環境引入(import, using),不實際點開檔案前,有誰會知道 "xxx.d" 代表什麼呢?

「如果一個變數名稱還需要註解的輔助,那麼這個名稱就不具備展現意圖的能力」

避免誤導

  • 說明:
    當需要實作一群帳戶的變數名稱時,不要取 "accountList" 這類名字,除非該變數的資料結構真的是 Linked-List (萬一儲存帳戶的資料結構是 Hash 或 Tree 呢?)
  • 使用 "accountGroup"、"bunchOfAccounts"、"accounts" 都比 "accountList" 好

「避開使用那些與原意圖相違背的常見單詞」

產生有意義的區別

  • 說明:
    不要使用無意義的數字序列命名 (a1, a2, ..., aN)。假使名稱必須有所不同,程式才能編譯成功,那麼變數名稱也應該代表著不同意義才是
  • 例如:
    def get_dist(a1, a2):
        return a2 - a1
    
    # Which Do you Like?
    
    def get_dist(source, destination):
        return destination - source
    

P.S. 筆者真的有在工作場合看過取名為 List1, List2, List3 這樣的程式碼...,也許改成ListForXXX, ListForYYY, ListForZZZ 會是更好一點的作法

使用可被搜尋的名字

  • 說明:
    當全域搜尋某變數時,我們不會希望跳出一大堆與目標無關的內容,只因他們的名稱太類似、甚至一樣 (白話文: 菜市場名)。在命名的時都要問自己: 如果未來想搜尋這個名稱、是否能馬上就找到?

  • 不用擔心影響到打字時間,現代開發工具補齊功能都很完善

  • 例: 如果我們未來想搜尋 "34"、"5" 出現在哪,試問專案搜尋結果會出現幾種可能性?

    int s = 0;
    for (int i = 0; i < 34; ++i){
        s += t[i] / 5;
    }
    
  • 而我們多宣告了 2 個變數,就能大大地減少變數搜尋時間:

    const int NUMBER_OF_TASKS = 34;
    const int WORK_DAYS_PER_WEEK = 5;
    int sum = 0;
    
    for (int i = 0; i < NUMBER_OF_TASKS; ++i){
        sum += tasks[i] / WORK_DAYS_PER_WEEK;
    }
    

「就這一點而言,長命名勝過短命名」

類別 (Class) 與方法 (Method) 的命名

  • 類別和物件宜使用名詞來命名
    • 例如: AddressParser、Customer
    • 避免 Data、Info、Manager、Processor 這類含糊的詞彙
    • 不要使用動詞
  • 方法應該使用動詞來命名,且:
    • 取出器 (accessors): 使用 get 當字首
    • 修改器 (mutators): 使用 set 當字首
    • 判定器 (predicates) 使用 is 當字首
  • 例如:
    class Employee
    {
    public:
        void setEmployeeName(string empName);
        string getEmployeeName() const;
    
        void setEmployeeId(string empId);
        int getEmployeeId() const;
    
        bool isEmployeeNameValid();
        bool isEmployeeIdDuplicate();
    private:
        private string employeeName;
        private int employeeId;
    }
    

每個概念都只使用一個詞

  • 不同類別的存取方法,與其分別取為 "fetch"、"retrieve"、"access",不如統一取作 "get"
  • DeviceManager (裝置管理者) 和 ProtocolController (協定控制器)、Driver (驅動程式) 之間的實質差別是什麼呢? 是否能統一使用 Controller 或 Manager?

「替單一抽象概念挑選一個字詞,並堅持持續地使用它」

其他

  • 使用能唸出來的名稱
    可幫助大腦記憶,口語溝通上也較為方便
  • 不要裝可愛
    例: 不要取 "HolyGrenade()" 這類意義不明的名稱
  • 避免雙關語
    例: "add" 有加法與 append, insert 的意圖,建議開發團隊先行規範好
  • 優先使用電腦領域(Computer Science)的詞彙
    閱讀你程式的人通常都是程式設計師,我們不希望來來回回向顧客詢問每個字的業務涵義

CH4: 註解 (Comments)

「我們需要註解,因為我們無法每次都找到不使用註解就能表達意圖的方法,但使用註解並不值得慶賀」

取自: Clean Code (p.62)

  • 註解是一種必要之惡,能愈少就愈少
    • 好的程式不需要太多註解也能讀懂,註解無法彌補糟糕的程式碼!
    • 一個註解存在的時間愈久,事實就愈偏離當初的程式碼解釋,甚至可能完全就是個誤導
    • 原因很簡單,程式設計師並沒有確實維護他們 (沒錯,註解也是需要"維護"的...)

用程式碼表達你的本意

  • 算一下你花了幾秒才理解這段 Code 的意義?

    // Check to see if the employee is eligible for full benefits
    if ((employee.flags & HOURLY_FLAG) && 
        (employee.age > 65))
    {
        // Do Something
    }
    else
    {
        // Do Something
    }
    
  • 從上面的 Code 我們可觀察到:

    • 條件判斷式之內放了太多細節 (若有 4 個條件需判斷,if 要寫幾行呢?)
    • 讀者是否能一眼看出 else 成立的條件?
    • 由於判斷式不夠精簡,工程師試圖使用額外的註解去解釋區塊(Block)的行為
    • 65 這年齡的意義為何? 若不懂退休制度的人看到 65 是否會困惑一下?
    • 每一個國家的退休年齡都一定是 65 嗎? 若未來我國修法,所有使用到 65 的地方好修正嗎?
  • 接著看看下面的 Code (筆者針對 Clean Code p.63 進行的補充,如有不足敬請指點)

    if (employee.isEligibleForFullBenefits())
    {
        // Do Something
    }
    else
    {
        // Do Something
    }
    
    
    bool isEligibleForFullBenefits()
    {
        bool isApplyRetire = employee.flags & HOURLY_FLAG;
        bool isEligibleForRetire = employee.age > RETIRE_AGE_LIMIT;
    
        return isApplyRetire && isEligibleForRetire;
    }
    

NOTE: 某一些程式語言 (例如 C#) 可能會習慣在每一個 Method 或 Variables 之上寫些 Doc 型註解

  • 例如:
    /// <summary>  
    /// To calculate total salary.  
    /// </summary>  
    /// <param name="salary"></param>  
    /// <param name="bonus"></param>  
    public void GetEmployeeSalary( int salary, int bonus)  
    {  
        int totalSalary = salary + bonus;  
        return totalSalary;
    }
    
    這種情形建議還是遵照慣例會比較妥當,以利後續 API 文件的自動產生
    就筆者目前的理解是,大範圍 (Scope) 的類別或方法可以寫多一點註解,但是內部功能的實作細節不宜有過多註解,最好是能用程式碼就表達整個行為意圖

好的註解

  • 法律型註解

    // utility.cc 
    //	Debugging routines.  Allows users to control whether to 
    //	print DEBUG statements, based on a command line argument.
    //
    // Copyright (c) 1992-1993 The Regents of the University of California.
    // All rights reserved.  See copyright.h for copyright notice and limitation 
    // of liability and disclaimer of warranty provisions.
    
    #include "copyright.h"
    #include "utility.h"
    

    但如果可能,這樣的註解內容不應直接是契約或條款,建議讓它去參考一個外部文件

  • 對意圖的解釋 & 後果的告誡

    currentThread->Finish();	// NOTE: if the procedure "main" 
                                // returns, then the program "nachos"
                                // will exit (as any other normal program
                                // would).  But there may be other
                                // threads on the ready list.  We switch
                                // to those threads by saying that the
                                // "main" thread is finished, preventing
                                // it from returning.
    return(0);			        // Not reached...
    

    上述的註解特別提醒了 Thread 完成後需要注意的事項,尤其我們可以知道 return 是不會馬上被觸發到的

  • TODO (待辦事項) 註解
    大部分的 IDE 都提供 Task Tags 查找的機制來找出所有的 TODO 註解。建議定期地審視這些待辦事項,並且刪除已經不再需要的待辦事項

衍生閱讀: 特殊註解:TODO、FIXME、XXX

壞的註解

  • 誤導型的註解
    絕對不要寫下錯誤的註解!!!

  • 被註解起來的程式碼
    這是很討人厭的,別這樣做!!!

  • 日誌型註解 & 出處和署名
    現代版控工具都很發達,別再用註解寫日誌

  • 多餘的註解
    別讓讀註解比讀程式碼還費時

  • 其他
    位置標誌 (效果非常顯著時才使用,否則只是凌亂物)

    {
        // code...
    
    // Actions //////////////////////////
    
        // code...
    }
    

    右大括號後面的註解

    while (...)
    {
        // code...
    } // end while
    

CH5: 編排 (Layouts)

「程式的編排是很重要的。編排是一種溝通方式,而溝通是專業開發者的首要之務」

取自: Clean Code (p.86)

垂直編排

  • 經驗法則: 許多 200 ~ 500 行 的程式碼檔案足以組成重要大型系統
  • 要像讀報紙一樣
    • 階層式架構
    • 慢慢浮現: 高階概念 -> 演算法 -> 程式細節
  • 垂直空白區隔不同的概念
    • 類似的概念盡可能靠近 (Conceptual Affinity)
    • 每一小段 Code 代表一個完整思緒
    • 用空白分隔這些思緒、空白行的後方接續一個新而不同的概念
  • 垂直距離
    • 密度表示功能密切相關的程度
    • 變數的宣告盡可能靠近使用處
    • 相依的函式 (Dependent Functions) 盡可能靠近
      • 例: FuncA call FuncB call FuncC,則他們的編排應當由上而下

水平編排

  • Code 該多寬?
    • 原則:不會用到水平卷軸 (一目了然)
    • 寬度建議不超過 120字元
  • 空白間隔和密度可用來表示關聯強弱度
    /* Eaxmple 1 */
    (-b+Math.sqrt(determinant))/(2*a);
    // vs.
    (-b + Math.sqrt(determinant)) / (2*a);
    
    /* Eaxmple 2 */
    return b*b-4*a*c;
    // vs.
    return b*b - 4*a*c;
    
  • 水平縮排與概念層級相關
    • 高階概念靠左,實作細節往右縮

Reference

  1. Important Tips To Write Clean Code In C#
  2. NACHOS Source Code

上一篇
Day 02: 給全端開發者的 Coding Conventions & Style Guide 補充
下一篇
Day 04: 函式、錯誤處理
系列文
成為乾淨的開發者吧! Clean Code, Clean Coder, Clean Architecture 導讀之旅31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言