0%

代码整洁之道1 - 命名

前言

《代码整洁之道》的第二章读书笔记整理,示例代码为C#, 逻辑大部分仿照原文使用的Java代码,加入了一些自己思考后的示例。

正文

1. 名副其实


要让别人一看变量、函数或类的名称就该知道它是啥、它做了什么事、它应该怎么用。

假设有一个与时间相关的变量

1
int d; 

如果直接这么命名,第一次看到这变量的人不容易理解,更好的命名方式如下:

1
2
3
int elapsedTimeInDays;
int daysSinceCreation;
int fileAgeInDays;

这样变量所代表的含义一目了然。

以下代码同理,函数名含义模糊不清,另外列表list1 、常量4、迭代变量x等的意义未知。

1
2
3
4
5
6
7
8
9
10
public List<int[]> getThem()
{
List<int[]> list1 = new List<int[]>();
foreach(int[] x in theList)
{
if(x[0] == 4)
list1.Add(cell);
}
return list1;
}

以下为重构后的代码,使用一个类来封装相关的变量与逻辑。

1
2
3
4
5
6
7
8
9
10
public List<Cell> getFlaggedCells()
{
var flaggedCells = new List<Cell>();
foreach(var cell in gameboard)
{
if(cell.isFlagged())
flaggedCells.Add(cell);
}
return flaggedCells;
}

2. 避免误导


  • 避免使用与本意相悖的单词,比如 UniX平台下的 hpaixsco 等专有名称不应作为变量名。
  • 避免使用形如accountList 的名称,除非该变量本身就是List类型,考虑到未来该容器的类型可能会发生改变(比如变成了DictionaryHashSet之类的),这个时候accountList的命名就会引起其他开发人员错误的认知,他第一次看到该变量被调用时会认为这是一个List
  • 更好的命名是用 accountGroupbunchofAccounts,甚至直接用accounts都行。

3. 命名要有区分


  • 在同一作用范围内的命名不能太相似。比如

    1
    2
    3
    4
    5
    public static void copyChars(char[] a1, char[] a2)
    {
    for(int i = 0; i < a1.Length; ++i)
    a2[i] = a1[i];
    }

    上述代码的参数名a1a2完全可以用sourcedestination替代,如此的话函数使用者就知道他是将第一个数组的数据拷贝到第二个数组当中。

  • 避免意义相近的名称,如ProductDataProuctInfo这种。

4. 可读性良好的命名


  • 使用英文单词对变量进行命名,不要用各种简写或者拼音,以下为不好的命名示例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class DtaRcrd102
    {
    private DateTime genymdhms;
    private DateTime modymdhms;
    private readonly string pszqint = "102";
    };

    class Wuqi
    {
    private float chongnengshijian; //充能时间
    private int danjiarongliang; //弹夹容量
    }
  • 纠正过后的良好命名如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class CustomerRecord
    {
    private DateTime generationTime;
    private DateTime modificationTime;
    private readonly string recordId = "102";
    };

    class Weapon
    {
    private float chargeTime;
    private int clipSize;
    }

5. 可搜索命名


尽可能地使用常量定义和单词变量去替代纯数字或者纯字符串,这样其他人想修改值的时候只需要搜索到定义的地方修改就行,不用一个个去找(比如下面这段代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Player
{
private List<Weapon> m_Weapons = new List<Weapon>();

public void AddWeapon(Weapon newWeapon)
{
if(m_Weapons.Count >= 5)
return;
m_Weapons.Add(newWeapon);
}

public Weapon GetWeaponAt(int idx)
{
if(idx < 0 || idx >= 5)
return;
return m_Weapons[idx];
}
}

可以优化为更容易搜索和维护的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Player
{
private List<Weapon> m_Weapons = new List<Weapon>();

private const int MAX_WEAPON_NUM = 5;

public void AddWeapon(Weapon newWeapon)
{
if(m_Weapons.Count >= MAX_WEAPON_NUM)
return;
m_Weapons.Add(newWeapon);
}

public Weapon GetWeaponAt(int idx)
{
if(idx < 0 || idx >= MAX_WEAPON_NUM)
return;
return m_Weapons[idx];
}
}

这样当后续想要修改最大武器数量只需要修改MAX_WEAPON_NUM处即可,也便于维护者按关键字max之类的搜索。

6. 避免使用多余的编码


  • 6.1 匈牙利语标记法
    • 即在变量名前添加类型字符。比如iCount表示int类型的Count变量、pUnit表示指向一个Unit类的指针变量等,详细参见百科上的说明
    • 起源是在Windows的C语言API的时代,编译器不做类型检查,程序员需要这种方法来区分句柄、长指针或者void指针等变量的类型。现代高级语言一般不需要这样来做,作者也不建议这么来命名了。
    • 个人认为对于LuaPython 这种非强类型的脚本语言来说,这种命名还是可以采取的。
  • 6.2 成员前缀
    • 在类的成员变量(member variables)前添加 m_ 后缀, 如上文的Player 里定义的m_Weapons
    • 原文作者不建议使用成员前缀。这一点个人不是很赞同, 对于C#来说,在类的成员变量和索引器较多的情况下,还是应该引入前缀来区分。简而言之,视使用环境而定,我个人偏好使用成员前缀。
  • 6.3 接口命名
    • 作者也不建议在接口命名前添加大写字母I,比如比起IShapreFactory作者更青睐ShapeFactory。这一点个人也不是很赞同,私以为用这种方式区分好接口类和普通类还是有必要的。
  • 6.4 中文(个人补充)
    • 一般来说命名也不要用中文,不同机器如果默认编码不同会经常导致中文乱码。

7. 避免思维映射


不要让代码阅读者在脑中把你的命名翻译为他们熟悉的名称,通俗点来说就是不要让命名产生不必要的联想。

8. 命名的词性选择


  • 类名、对象名应使用名词或者名词短语,比如 CustomerWikiPageAccount
  • 方法、函数使用动词或者动词短语,比如postPaymentdeletePageSave等。

9. 不要扮可爱

不要取一些自己认为很cool的名字,不要抖机灵。比如一个简单的DeleteItem函数不要写成WipeOutTheUseless之类的。

10. 命名概念明确单一


  • 不要混用相同概念的词,每个抽象概念选一个词,并且一以贯之。比如不要在项目中同时有xxxManagerxxxControllerxxxDriver这类相同意义的名词,选其中一种就行了。
  • 不要在不合适的情况下使用双关语。比如 Add 函数这种,在一个类当中它可能表示将两个值相加,也可能表示将一条数据条目添加到指定集合当中,如果出现了这种情况可以考虑换成Append或者Insert

11. 使用项目开发人员都熟悉的名称


  • 使用解决方案领域的名称。
  • 使用源自所涉问题领域的名称。

12. 符合语境


  • 添加有意义的语境

    • 一个函数中出现了state变量,如果代码阅读者最开始理解不了这个变量的意义,他就得去定向到这个state的定义处,结合定义所在行的上下文来理解,导致花费更多的时间来熟悉代码。
    • 比如在一个名为Player的类里,state可能是指AnimationState,也可能是GameState,那么最好按照逻辑功能将使用的变量命名为对应的animStatecurrentGameState之类的,以此避免他人花费更多的时间去理解上下文。
  • 避免无意义的语境

    • 避免在某些类前面加上应用名称的前缀,比如一个加油站豪华版应用(Gas Station Deluxe)中的每个类都加了GSD前缀,类似GSDAccountAddress这种,本来只是一个表示邮件地址的类,加了前缀后会让其他人误以为是用来什么特殊处理逻辑。
    • 这个问题个人也在实际开发过程中遇见过:在前东家游戏项目内阅读相关的C++源码时发现了很多带ES前缀的类(比如ESArrayESString之类的),实际代码逻辑是一些针对项目本身需求优化过的STL容器,但一直很疑惑ES是啥特殊含义,还跑去问了上级老大,结果被告知ES是公司的英文简写。 这种容易造成误解的情况还是尽可能避免,实在需要的话,可以用命名空间包一层来处理。

后记

整个第二章读下来,发现大部分坑已经在以前做项目的过程中趟过了,不过还是学到了新的东西,可见理论和实践还是应该相辅相成才能对个人给予最大的成长。