Day 03: 有意义的命名、好的注解、垂直 & 水平编排

「我们是认真严肃地看待命名这件事,请您牢记这一点」

取自: 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 4 - 安全签章: 讯息内文杂凑

>>:  Day3 AR其实在生活中很常见?他们又有那些好处哩(成为史莱姆猎人的萌新)

[经典回顾]网路异常疑机房失火,老板:「不是有防火墙?」

资料来源: 为什麽没有「防火墙」? 火灾袭「是方电讯」!某天然呆老板:不是有防火墙吗? 内湖机房失...

Day16 - this&Object Prototypes Ch3 Objects - Iteration 开头

作者说明 forEach()、every()、some() 三者在跑回圈运作上的差异 forEach...

NNI如何搬到Colab01

NNI搬到Colab上,环境类似本机。虽然,NNI很容易搬到Colab平台上,但由於Colab并不公...

Day30 ( 高级 ) 显示声波图形

显示声波图形 教学原文参考:显示声波图形 这篇文章会介绍,在 Scratch 3 里侦测麦克风的声音...

Day28 ATT&CK for ICS - Command and Control

Command and Control 攻击者已经进入工控环境之後,从自己的服务器传送指令给受害主机...