《代码简洁之道》阅读笔记

内容摘选自 《代码简洁之道》,作者:Robert C. Martin。仅供学习交流使用,后续会持续进行更新。
本次更新时间:2023年11月27日
下次更新:5天内

序言

本书提出了一种概念:代码质量与其整洁度成正比。

第 1 章 整洁代码

阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好的程序员。

1. 什么是简洁代码

Bjarne Stroustrup, C++ 语言发明者:我喜欢优雅和高效的代码。代码逻辑应直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

Grady Booch, Object Oriented Analusis and design with applications 作者:整洁的代码简洁直接。整洁的代码如同优美的散文,充满干净利落的抽象和直接了当的控制语句。

光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随着时间流逝而腐坏。我们应该更积极地阻止腐坏的发生。“让营地比你来时更干净”。

第 2 章 有意义的命名

我们给变量、函数、参数、源代码等等命名。我们命名、命名,不断命名。既然有这么多命名要做,不妨做好它。下文列出了取个好名字的几条规则。

2.1 名副其实

取名字这件事很 严肃。选个好名字要花时间,但是省下来的时间要比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。

如果名称需要注释来补充,那就不算名副其实

下面举几个例子:

int d;  // 消逝的时间,以日计

名称 d 什么也没有说明,它没有引起对时间消逝的感觉,更别说以日计了。我们需要指明计量对象和计量单位的名称。选择体现本意的名称能让人更容易理解和修改代码。

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

下面的代码意义是什么?

public List<int[]> getThem() {
    List<int[]> list = new ArrayList<int[]>();
    for (int[] x : theList)
        if (x[0] == 4) {
            list.add(x);
        }
    return list;
}

问题不在于代码的简洁度,而是在于代码的 模糊度:即上下文在代码中未被明确体现的程度。对于上述代码,我们有以下疑问:

  • theList 中是什么类型的东西?
  • theList 零下标条目的意义是什么?
  • 值 4 的意义是什么?
  • 我怎么使用返回的列表?

问题的答案没有体现在代码段中。

假如,我们在开发一种扫雷游戏,我们发现,盘面是名为 theList 的单元格列表,那就将其名称改为 gameBoard。

盘面上每个单元格都用一个简单数组表示。我们还可以发现,零下标条目是一种状态值,而这种状态值为 4 表示“已标记”。只要改为有意义的名称,代码就会得到相当程度的改进:

public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList<Cell>();
    for (Cell cell : gameBoard)
        if (cell.isFlagged())
            flaggedCells.add(cell);
    return flaggedCells;
}

只要简单的修改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。

2.2 避免误导

程序员必须避免留下隐藏代码本意的错误线索。应该避免使用与本意相悖的词。hpaix、和 sco 都不应该做变量名,因为他们是 UNIX 平台或者类 UNIX 平台的专有名称。

别用 accountList 来指称一组账号,除非它真的是 List 类型。

提防使用不同之处较小的名称。想区分模块中某处的 XYZControllerForEfficientHandlingOfStrings 和另一处的 XYZControllerForEfficientStorageOfStrings ,会花多次时间呢?这两个词的外形实在是太相似了。

2.3 做有意义的区分

以下面的代码为例。

public static void copyChars(char a1[], char a2[]) {
    for (int i=0; i<a1.length; i++) {
        a2[i] = a1[i];
    }
}

如果参数名改为 source destination,这个函数就更容易理解一点。

废话是另一种没有意义的区分。假如你有一个 Product 类。如果还有一个 ProductInfo ProductData 类,那它们的名称虽然不同,但是意义却没什么差别。InfoData 就像 a、anthe 一样,是意义含混的废话。

2.4 使用读的出来的名称

如果名称读不出来,讨论的时候就会像个傻鸟。

比如写了一个 genymdhms (生成日期,年、月、日、时、分、秒),一般会读成(gen why emm dee aich emm ess)。这听起来傻乎乎的,不如直接改成 generationTimeStamp

现在读起来就像人话了:“喂,Mikey,看看这条时间戳!”

2.5 使用可搜索的名称

单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。

MAX_CLASSES_PER_STUDENT 很容易,但是找数字 7 就很麻烦了,它可能是某些文件名或者其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。

若常量或者变量在代码中多出使用,则应赋其以便于搜索的名称。例如:

for (int j=0; j<34; j++) {
    s += (t[j] * 4) / 5;
}

// 可搜索的名称
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j<NUMBER_PF_TASKS; j++) {
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    int realTaskWeeks = (realDays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}

注意,上面代码中的 sum 并非特别有用的名称,不过它至少搜得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK5 要好找的多。

2.6 避免使用编码

类型 或者 作用域 写进名称中,陡然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外,还要再搞懂另一种编码语言。这对于解决问题而言,纯属多余的负担。带编码的名称也不便发音,容易打错。

2.6.1 匈牙利语标记法

据说这种命名法是一位叫 Charles Simonyi 的匈牙利程序员发明的,后来他在微软待了几年,于是这种命名法就通过微软的各种产品和文档资料向世界传播开了。匈牙利语法的格式一般为:变量名=属性+类型+对象描述

例如:

  • hwnd: h 是类型描述,表示句柄,wnd 是对象描述,表示窗口,所以 hwnd 表示窗口句柄。
  • g_cchg_ 是属性描述,表示全局变量,cch 分别是计数类型和字符类型,一起表示变量类型,这里忽略了对象描述,所以它表示一个对字符进行计数的全局变量。

匈牙利语标记法百度百科

早期的编译器不会对变量做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。

现代编程语言有更丰富的类型系统,编译器也记得变量的类型,代码环境已经先进到再变异开始之前就侦测到类型错误的程度!所以,这种类型编码纯属多余。他们增加了修改变量、函数或类的名称或类型的难度。他们增加了代码阅读的难度。

这里补充现在几个常见的编码类型:

驼峰式命名法:第一个单词首字母小写,后面其他单词首字母大写。java 中的变量命名就经常使用驼峰命名法。

char[] myName;
int myAge;

帕斯卡命名法:又叫大驼峰命名法,每一个单词的首字母都大写。c# 中使用的比较多。

char[] MyName;
int MyAge;

下划线命名法:单词与单词之间用下划线连接,所有字母都小写。python 中使用的较多。

my_name = 'zs';
my_age = 1;

2.6.2 成员前缀

不必使用 m_ 前缀来标明成员变量。应该把类和函数做的足够小,消除对成员前缀的需要。你应该使用某种可以高亮或者用颜色标出成员变量的编码环境。

public class Part {
    private String m_dsc;  // 不要使用这种方式
    private String description;  // 推荐方式
}

此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分。代码读的越多,眼中就越没有前缀。最终,前缀变成了不入法眼的废料,变成了旧代码的标志物。

2.6.3 接口和实现

有时候也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是一个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?IShapeFactoryShapeFactory 吗?

我更倾向于不加修饰的接口。前导字母 I 被滥用到了说好听点是干扰,说难听点就是废话。如果接口和实现必须要选择一个来编码的话,我宁肯选择实现。ShapeFactoryImp 比对接口编码要好的多。

2.7 避免思维映射

不应该让读者在脑中把你的名称翻译为他们熟知的名称。

单字母的变量名就是个问题。在作用域较小,也没有名称冲突时,循环计数器自然有可能被命名为 ijk。这个时候对于其他变量的命名就不宜再使用 l 这样的单字母了,因为会被认为是某个计数器。

专业程序员能善用其能,编写其他人能理解的代码。

2.8 类名

类名和对象名应该是 名词 或者 名词短语,如 CustomerWikiPageAccountAddressParser。避免使用ManagerProcessorDataInfo 这样的类名,因为他们指代不清楚,可以修改为名词短语。如 HotelManager。类名不应当是动词。

2.9 方法名

方法名应该是 动词 或者 动词短语,如 postPaymentdeletePagesave

String name = employee.getName();
employee.setName('mike');

2.10 别扮可爱

如果名称太耍宝,那就只有同作者一样有幽默感的人才能记住,而且还是在他们记得那个笑话的时候才行。谁会之道 HolyHandGrenade 的函数是干什么的呢?没错,这个名字挺伶俐的,不过 DeleteItems 或许是更好的名称。宁可明确,毋为好玩。( HolyHandGrenade,意味圣手手雷,破坏除防爆物体以外的物品。DeleteItems,删除条目。)

扮可爱的做法在代码中常体现为使用俗话或者俚语。例如,别用 whack() 表示 kill()。别用 eatMyShorts()这类与文化相关的笑话来表示 abort()。(whack,劈砍。eatMyShorts,去死吧)

2.11 每个概念对应一个词语

给每个抽象概念选一个词,并一以贯之。

例如,使用 fetchretriveget 来给在多个类中的同样方法命名。你怎么记得哪个类中是哪个方法呢?

同样,在同一堆代码中有 controller,又有 manager 就会令人困惑。DeviceManagerDeviceController 之间有什么区别呢?这让人觉得这两个对象是不同类型的,也分属不同的类。

对于那些会使用到你代码的程序员,一以贯之的命名法简直就是泼天富贵。

2.12 别用双关语

避免将同一单词用于不同目的。同一术语用于不同的概念,基本上就是双关语了。

比如,我们有一个 add() 方法,该方法将通过增加或者连接两个现存的值来获得新值。假如现在又有一个add()方法,需要实现将一个值添加到一个群组中(方法三)。

# 方法一,将某个值 +1
value = 0
def add():
    global value  # python代码,代表使用外部的变量
    value += 1

# 方法二,返回两数之和
def add(val1, val2):
    return val1 + val2

# 方法三,添加某个值到群组中
def add(my_list, value):
    my_list.append(value)

方法三貌似和其它方法保持了一致,但实际语义不同,应该用 insert 或者 append 之类的词来命名。把该方法命名为 add,就是双关语了。

2.13 使用领域解决方案名称

记住,只有程序员才会读你的代码。所以,尽管使用那些计算机科学属于、算法名、模式名、数学术语吧。

对于熟悉访问者 ( VISITOR ) 模式的程序员来说,名称 AccountVisitor 富有意义。哪个程序员会不知道 JobQueue 的意思呢? 程序员要做太多技术性工作。给这些事取个技术性的名称,通常是最靠谱的做法。

如果不能使用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能请教领域专家了。

2.14 添加有意义的语境

很少有名称是能自我说明的 ------ 多数都不能。反之,你需要有良好命名的类、函数或者名称空间来放置名称,给读者提供语境。如果你没有这么做,给名称添加前缀就是最后一招。

设想你有名为 firstNamelastNamestreethouseNumbercitystatezipcode 的变量。当它们搁一块的时候,很明确是构成了一个地址。不过,假使只看到一个 state 变量呢?你会理所当然推断那是某个地址的一部分吗?

你可以添加前缀 addrFirstNameaddrLastNameaddrState 等,以此提供语境。至少读者会明白这些变量是某个更大结构的部分。当然了,更好的方案是创建名为 Address 的类。这样,即便是编译器也会知道这些变量隶属于某个更大的概念了。

看看下面的代码。以下变量是否需要更有意义的语境呢?函数名仅给出了部分语境;算法提供剩下的部分。浏览函数后,你才会知道 numberverbpluralModifier 这三个是猜测的信息的一部分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些变量的含义完全不清楚。

// 语境不明确的变量
private void printGuessStatistice(char candidate, int count) {
    String number;
    String verb;
    String pluralModifier;
    if (count == 0) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    } else if (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    } else {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
    String guessMessage = String.format("There %s %s %s%s", verb, number, candidate, pluralModifier);
    print(guessMessage);
}

// 部分输出值
// There are 5 ps
// There is 1 m

上述的函数有点儿长,变量的使用贯穿始终。要分解这个函数,需要创建一个名为 GuessStatisticsMessage 的类,把三个变量做成该类的成员字段。这样它们就在定义上变成了 GuessStatisticsMessage 的一部分。语境的增强也让这个算法能够通过编写更小的函数而变得干净利落。

// 有语境的变量
public class GuessStatisticsMessage {
    private String number;
    private String verb;
    private String pluralModifier;
    
    public String make(char candidate, int count) {
        createPluralDependentMessageParts(count);
        return String.format("There %s %s %s%s", verb, number, candidate, pluralModifier);
    }
    
    private void createPluralDependentMessageParts(int count) {
        if (count == 0) {
            thereAreNoLetters();
        } else if (count == 1) {
            thereIsOneLetter();
        } else {
            thereAreManyLetters(count);
        }
    }
    
    private void thereAreNoLetters(){
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
    
    private void thereIsOneLetter(){
        number = "1";
        verb = "is";
        pluralModifier = "";
    }
    
    private void thereAreManyLetters(int count){
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
}
  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Python 代码笔记是对 Python 程序代码的解释和说明。它可以帮助你理解代码的工作原理,并在以后更好地维护和编写代码。常用的代码笔记格式有注释、文档字符串等。示例代码: ```python # 计算平方 def square(x): """ 返回x的平方 """ return x*x print(square(4)) ``` 在上面的代码中,`# 计算平方`是注释,`"""返回x的平方"""`是文档字符串。 ### 回答2: Python代码笔记是程序员在学习和实践Python编程语言时记录的一种文档。它包括通过编写实际的Python代码示例来记录各种语法、函数、模块、库和算法的用法和应用。 Python代码笔记通常用于记录和整理编程语言的基本知识,并用代码示例来演示这些知识的具体使用。因为Python语言本身较为简洁易读,因此在代码笔记中使用Python语言编写示例代码非常方便。 通过编写Python代码笔记,程序员可以更好地理解和掌握Python编程语言的特性和用法。而且代码笔记还可以作为程序员的参考资料,帮助他们在遇到问题时快速找到解决方案并进行复用。 除了记录基本知识之外,Python代码笔记还可以用于记录程序员在实际项目中遇到的问题和解决方案。通过记录这些问题和解决方案,程序员可以在未来的项目中预防和避免相同的问题,并且能够提高自己的编程技巧和经验。 总之,Python代码笔记是程序员学习和实践Python编程语言时记录的一种文档。它可以帮助程序员整理知识、提高编程技巧,并成为他们解决问题和提高效率的有力工具。 ### 回答3: Python代码笔记是程序员在学习和使用Python语言时记录的一种方式。它可以包括以下内容: 首先,Python代码笔记通常会记录Python代码的基本语法和用法。这些笔记会列举Python的关键字、变量类型、运算符、控制流语句等基本知识点,以便在需要的时候进行快速查阅和复习。 其次,Python代码笔记还会记录一些常用的Python库和模块的使用方法。Python具有丰富的第三方库和模块,如numpy、pandas、matplotlib等,这些库在数据处理、科学计算、绘图等领域都有广泛的应用。通过记录库和模块的使用方法,可以帮助程序员实现特定的功能或解决具体的问题。 此外,Python代码笔记还会记录一些常见的编程技巧和经验。比如如何提高代码的效率、如何优化算法、如何进行调试等等。这些技巧和经验是程序员在实际开发中积累的宝贵资料,可以帮助他们更好地解决问题和提高工作效率。 最后,Python代码笔记还可以记录一些项目示例和实践经验。当程序员在开发具体的项目时,他们会遇到各种问题和挑战,记录下来的项目示例和实践经验可以为他们以后的开发工作提供参考和借鉴。这些实践经验可以包括项目的架构设计、数据库操作、接口调用等方面的知识。 综上所述,Python代码笔记是程序员学习和使用Python语言的重要辅助工具,它通过记录基本语法、常用库和模块的使用、编程技巧和经验以及项目示例和实践经验等内容,帮助程序员提高开发效率,解决问题,并不断提升自己的编程能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值