《编写可读代码的艺术》读书笔记

news/2025/2/2 9:27:56 标签: 可读代码

1. 写在前面

借着春节放假的几天, 读了下《编写可读代码的艺术》这本书, 这本书不是很长,主要关注代码的一些编写细节,比如方法命名,函数命名,语句组织,任务分解等, 旨在让写的代码更加鲁棒,便于维护以及便于让别人理解。写的还是很不错的,所以这边文章整理一些不错的编码细节。

大纲如下:

  • 1. 写在前面
  • 2. 表面层次的改进
    • 2.1 起名字
    • 2.2 不会误解的名字
    • 2.3 审美
    • 2.4 注释
  • 3. 简化循环和逻辑
    • 3.1 控制流变得易读
    • 3.2 拆分超长的表达式
    • 3.3 变量与可读性
  • 4. 重新组织代码
    • 4.1 抽取不相关的子问题
    • 4.2 一次只做一件事情
    • 4.3 想法变成代码
    • 4.4 少写代码
  • 5. 测试与可读性
  • 6. 小总

2. 表面层次的改进

我们写代码的时候,考虑的最重要的一个原则应当是易于让别人理解的,代码的写法应当使得别人理解它使用的时间最小化, 这是作者书中提到的可读性的基本原理, 提高代码的可读性在实际中非常重要, 我们可以先从表面层次开始。

表面层次包括:选择好的名字、写好的注释、把代码整洁成更好的格式

2.1 起名字

关键思想:

给变量、函数、类起名字的时候, 要把信息装在名字中

介绍了6个常用的小方法:

  1. 起名字的时候,选择比较专业的词,避免"空洞"的次, 名字表达的信息越具体越好

    # 一个反例
    def GetPage(url)
    
    # get 这个词没有很多信息,这个方法是从本地缓存中得到页面,还是数据库,还是网页?  get比较宽泛,更专业的词FetchPage or DownloadPage等,更加具体
    

    另外可以找更有表现力的词, 清晰和精确比装可爱好。 比如:send -> deliver,dispatch,announce, find -> search, extract,locate, start -> launch, create, begin, open make -> create, build, add

  2. 好的名字应当描述变量的目的或它所承载的值,少用tmp和retrtval这样泛泛的名字

    # retval 除了表示返回一个值, 没有更多信息
    # 如果想计算平方和, 用sum_squares 比 retval要好很多
    
    # tmp这个名字也只应用于短期存在且临时性为主要存在因素的变量
    
  3. 循环迭代器里面往往喜欢用i,j,k这样的变量, 这个在多层循环里面不是个好的主意, 可以加上迭代器变量自身的信息

    for i in range(len(clubs)):
    	for j in range(len(clubs[i].members)):
    		for k in range(len(users)): 
    			....
    
    # 换成
    for ci in range(len(clubs)):
    	for mj in range(len(clubs[ci].members)):
    	 	for uk in range(len(users)):
    	 		...
    
  4. 具体的名字替代抽象的名字, 比如,假设有一个内部方法叫ServerCanStart(), 检测服务是否可以监听某个给定的TCP/IP端口。 但这个名字有点抽象,换成CanListenOnPort()更具体。

  5. 名字附带更多信息, 比如带上变量格式, 单位,其他重要的信息等

    # 格式
    id -> hex_id
    
    # 单位信息
    timestamp -> timestamp_ns
    delay -> delay_secs
    size -> size_mb
    limit -> max_kbps
    angle -> degree_cw
    
    # 其他重要信息: 如果变量表示的有需要理解的关键信息, 就把这个信息放到名字里面
    password -> plaintext_password
    comment -> unescapted_comment
    html -> html_utf8
    data -> raw_data
    
  6. 为作用域大的名字采用更长的名字,作用域小的用较短的名字, 使用缩略词和缩写的原则是团队的新成员是否能理解名字的含义, 比如把BackEndManager写成BManager可能更令人费解。

  7. 利用名字的格式来传递含义, 对不同的实体使用不同的格式,比如可以在类成员变量加_区分开普通变量

    # 有目的的使用大小写, 下划线等
    # 常量 AGE = 10
    # 类变量 age_
    # 普通变量  age
    

2.2 不会误解的名字

起名字时, 要考虑下是否会让别人产生误解,比如Filter这个名字, 可能过滤不好的,也可能保留好的,这样的名字直接看就有问题。类似的length, limit等。

这里也介绍了几个小技巧:

  1. 推荐用minmax来表示包含的极限, 即最大和最小取到多少, max_age, min_age
  2. 推荐用firstlast表示包含的范围
  3. 推荐用beginend表示起始位置和终止位置(终止位置的意思是最后一个元素的后一个位置,注意和上面的last区分)
  4. 给布尔值也命名,即赋予逻辑含义,比如is_xxx, has_xxx, can_xxx等,并且最好避免使用反义的名字,比如not_xxx,
  5. 小心用户对特定词的期望, 这个的意思是, 用户一般认为get(), size()这种方法是轻量级的方法, 可以直接索引到或用很简单的方式就能拿到,如果定义了一个方法,里面有比较大的计算量,就避免用get_xxx()来命名, 可以用count_xxx()

2.3 审美

审美主要是包括代码的布局, 这里介绍的一些技巧:

  1. 如果多个代码块作相似的事情, 尝试用同样的格式和布局
  2. 把代码按 “列” 对齐,可以让代码更容易浏览
  3. 如果一段代码中提到A,B和C, 后面就一直保持这个顺序
  4. 用空行把大块的代码,逻辑上分成多个段落

2.4 注释

注释的目的是尽量帮助读着了解的和作者一样多,所以注释的写法也是有讲究的。

  1. 不需要写注释的情况: 从代码本身就能快速推断的事实, 不好的名字

    # 类的定义
    class XXX
    	# 初始化
    	def __init__(self):
    		...
    
    # 上面的这种注释没有意义
    
    # 如果起的名字不好,不要写注释,而是把名字起好: 好代码 > 坏代码 + 好注释
    
  2. 注释的作用

    1. 记录思想: 比如为什么写成这样而不是那样的理由, 指导性批注, 一些背景知识等
    2. 代码中的瑕疵: 使用TODO, FIX, HACK等进行标记
    3. 常量加注释: 常量的介绍,为什么是这个值等
  3. 站在读者的角度

    1. 预料到代码中哪些部分会让读者看不懂
    2. 为普通读者意料之外的行为加注释
    3. 文件/类的级别上使用"全局观"注释来解释所有的部分是如何一起工作的
    4. 用注释来总结代码块, 使读者不迷失在细节中
  4. 写出言简意赅的注释

    1. 注释保持紧凑, 有很高的信息/空间率
    2. 避免使用it和this这样的代词
    3. 尽量精确的描述函数的行为
    4. 注释中用精心挑选的输入/输出的例子说明
    5. 声明代码的高层次意图,而非明显的细节
    6. 用嵌入的注释(如Function(/*arg=*/...))解释难以理解的函数参数
    7. 用含义丰富的词来使注释更简洁

3. 简化循环和逻辑

改变程序的“循环和逻辑”,可以让代码更有可读性。

一个原则:尽量减少代码中的"思维包袱", 思维包袱越多, 需要考虑复杂并记住更多事情,与容易理解恰好相反。

3.1 控制流变得易读

几种方法:

  1. 写一个比较时(while (bytes_expected > bytes_received)), 把改变的值写在左边并且把稳定的值写在右边会更好

  2. 可以重新排列is/else语句块, 一般先处理正确的/简单的/有趣的情况

  3. 某些变成结果,像三目运算符,do-while循环,goto最后不使用,会导致可读性变差

  4. 嵌套的代码块需要更加集中精力去理解,每层新的嵌套都需要读者把更多的山下文"压入栈", 应该改写成更"线性"的代码

  5. 提早return可以减少嵌套使得代码更加整结, 保护语句(函数顶部处理简单情况)很有用

    def xxx(value):
    	# 保护代码
    	if value == "xxx": return True
    	if xxx: return xxx
    	if xxx: return xxx
    
    	# 逻辑代码
    	...
    

3.2 拆分超长的表达式

几种方法:

  1. 引入"解释性变量",代表较长的子表达式

    username = line.split(":")[0].strip()
    if username == "xxx": 
    	...
    
    users_owns_docement = (requests.user.id == document.owner.id)
    if user_owns_docement: 
    	...
    
  2. 德摩根定理操作逻辑表达式 (if (!(a && b)) ⇒ if (!a || !b)

  3. 更复杂的逻辑表达拆分

3.3 变量与可读性

尽量减少变量的数量和让它们"轻量级", 来让代码更有可读性。

  1. 减少变量,即妨碍的变量,并减少中间结果

    now = datetime.datetime.now()
    root_message.last_view_time = now
    
    # 这个now在这里就没有意义, 没有拆分复杂的表达式,也没有做更多的澄清,只用过一次
    root_message.last_view_time = datetime.datetime.now()
    
  2. 减少每个变量的作用域, 越小越好,尽量避免使用全局变量,因为修改了可能不知道

  3. 只写一次的变量更好,比如const, final, 常量,使得代码更容易理解,一个变量操作的越多, 就越难确定值。

4. 重新组织代码

在函数级别对代码做更大的改动,使得逻辑更加清晰。

4.1 抽取不相关的子问题

写代码时, 要把和项目不相关的代码单独拿出来, 写成一个工具库供主程序调用。这样可以使得我们关注小而定义良好的问题。

纯工具代码Utils,独立的小模块,独立的小函数等都属于这个范畴, 这样在后期维护(添加功能,改进可读性,处理边界等)变得很容易。

现在我写代码的时候,一般会有utils目录,存放一些工具函数, consts存放一些常用变量等。

4.2 一次只做一件事情

forcus, 在写代码中非常使用,一个组织代码的技巧: 一次只做一个事情。如果有很难读的代码,尝试把它所做的所有任务先列出来,其中一些任务拆分成单独的函数 或 类, 其他的可以简单成为函数中的逻辑"段落", 小事情越具体,越容易拆分,也是很有挑战的一件事情。

4.3 想法变成代码

这里主要是介绍了一个简单的技巧:用自然语言描述程序 然后用这个描述来帮助写出更自然的代码, 这个就是把一个大的问题先拆解开,然后用自然语言去描述,有点像“如何把大象装进冰箱的意思"

4.4 少写代码

最好读的代码就是没有代码, 我们很容易乐观的估计一个粗糙原型所花的时间, 但忘了将来的维护时间和成本, 所以最后的一个建议是,不要过度设计项目,减少不必要的代码,重新考虑需求,解决版本最简单的问题,另外是经常性的阅读标准库的整个API,保持对它们的熟悉程度,不要重复造轮子

每隔一段时间, 花15分钟阅读标准库中的所有函数/模块/类型的名字

5. 测试与可读性

这一部分属于比较精细的话题了,如何写出有效且可读的测试。

几个要点如下:

  1. 每个测试的最高一层应该越简明越好, 最后每个测试的输入/输出可以用一行代码描述

    def CheckXXX(input, expected_output):
    	# 处理逻辑
    	# 最外层的测试函数不要有过多的实现细节
    	output = Getoutput(input)
    	assert (output == expected_output)
    
  2. 错误消息要尽量的有可读性,这样测试失败,可以更快跟踪问题, 比如python测试的时候,可以用unittest的测试库,比较专业

    import unittest
    class MyTestCase(unittest.TestCase):
    	def testFunction(self):
    		a = 1
    		b = 2
    		self.assertEqual(a, b)
    
    unittest.main()
    

    手工打造报错信息也可以。

  3. 使用最简单且能够完整运用代码的测试输入

  4. 给测试函数取一个完整描述性的名字,使得每个测试所测到的东西很明确,不要用Test1(), 用像Test_functionname_situation这样的名字

总之,测试要易于改动和增加新的测试。

6. 小总

过年在家, 用了3个晚上, 看完了这本书, 这本书篇幅不是很长, 读完了之后还是受益匪浅的, 里面有一些细节确实之前没有在意, 想用这篇文章大概把这本书的一些方法论整理出来,后面还是得需要实践去巩固,在实践的过程中慢慢的巩固这些方法论吧。

2025年的第一篇博客,新年快乐, 继续加油哇 😉


http://www.niftyadmin.cn/n/5839970.html

相关文章

一个数如果恰好等于他的因子之和,这是就成为“完数“,例如6=1+2+3.编程找出1000以内的所有完数

from sys import stdoutfor i in range(2,1001):k[] #用于存储因子si #初始化s为当前数字ifor j in range(1,i):if i%j0: #如果j是i的因子s-j #从s中减去银子jk.append(j) #将因子j加入列表kif s0:#如果s最终为0,说明i是一个完数print(i)for j in range(len(k)): #遍历银子列表…

Spring JDBC:简化数据库操作的利器

前言 Spring框架为Java开发者提供了多种技术解决方案,Spring JDBC作为其中的核心模块之一,帮助开发者更加轻松、简洁地进行数据库操作。本文将介绍Spring JDBC的概念、优势、如何使用以及常见的应用场景。 什么是Spring JDBC? Spring JDBC是…

浅析CDN安全策略防范

CDN(内容分发网络)信息安全策略是保障内容分发网络在提供高效服务的同时,确保数据传输安全、防止恶意攻击和保护用户隐私的重要手段。以下从多个方面详细介绍CDN的信息安全策略: 1. 数据加密 数据加密是CDN信息安全策略的核心之…

JavaScript前后端交互-AJAX/fetch

摘自千峰教育kerwin的js教程 AJAX 1、AJAX 的优势 不需要插件的支持,原生 js 就可以使用用户体验好(不需要刷新页面就可以更新数据)减轻服务端和带宽的负担缺点: 搜索引擎的支持度不够,因为数据都不在页面上&#xf…

2024第十五届蓝桥杯网安赛道省赛题目--cc(CyberChef)/crypto

打开链接后是: 通过题目界面可以知道是AES加密,并且告诉我们key是gamelabgamelab,IV是gamelabgamelab,Mode是CBC模式,输入是flag,输出为Hex十六进制4da72144967f1c25e6273950bf29342aae635e2396ae17c80b1b…

深度学习练手小例子——cifar10数据集分类问题

CIFAR-10 是一个经典的计算机视觉数据集,广泛用于图像分类任务。它包含 10 个类别的 60,000 张彩色图像,每张图像的大小是 32x32 像素。数据集被分为 50,000 张训练图像和 10,000 张测试图像。每个类别包含 6,000 张图像,具体类别包括&#x…

C++【深入底层,手撕vector】

vector是向量的意思,看了vector的底层实现之后,能够很明确的认识到它其实就是我们经常使用的顺序表。在我们的认知中,顺序表会有一个数组、数据的size以及容量的大小。vector作为一个向量容器,它可以存放任意类型的数据。所以在实…