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

server/2025/2/2 23:36:54/

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 Trueif xxx: return xxxif 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 = 1b = 2self.assertEqual(a, b)unittest.main()
    

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

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

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

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

6. 小总

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

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


http://www.ppmy.cn/server/164460.html

相关文章

通过OPC UA或MQTT协议,安全地将工业设备连接至物联网软件

尽管 Anybus Compact IIoT Secure 依然通过工业以太网协议(如 PROFINET 或 EtherNet/IP)与控制器交换数据,但它还可以使用 OPC UA 或 MQTT 协议,将数据传送到 IT 系统。这使得公司能够分析数据,而不需要额外开发定制软…

python3+TensorFlow 2.x(三)手写数字识别

目录 代码实现 模型解析: 1、加载 MNIST 数据集: 2、数据预处理: 3、构建神经网络模型: 4、编译模型: 5、训练模型: 6、评估模型: 7、预测和可视化结果: 输出结果&#xff…

论文笔记(六十三)Understanding Diffusion Models: A Unified Perspective(五)

Understanding Diffusion Models: A Unified Perspective(五) 文章概括基于得分的生成模型(Score-based Generative Models) 文章概括 引用: article{luo2022understanding,title{Understanding diffusion models: A…

AI编程工具使用技巧:在Visual Studio Code中高效利用阿里云通义灵码

AI编程工具使用技巧:在Visual Studio Code中高效利用阿里云通义灵码 前言一、通义灵码介绍1.1 通义灵码简介1.2 主要功能1.3 版本选择1.4 支持环境 二、Visual Studio Code介绍1.1 VS Code简介1.2 主要特点 三、安装VsCode3.1下载VsCode3.2.安装VsCode3.3 打开VsCod…

OpenEuler学习笔记(十五):在OpenEuler上搭建Java运行环境

一、在OpenEuler上搭建Java运行环境 在OpenEuler上搭建Java运行环境可以通过以下几种常见方式,下面分别介绍基于包管理器安装OpenJDK和手动安装Oracle JDK的步骤。 使用包管理器安装OpenJDK OpenJDK是Java开发工具包的开源实现,在OpenEuler上可以方便…

《大数据时代“快刀”:Flink实时数据处理框架优势全解析》

在数字化浪潮中,数据呈爆发式增长,实时数据处理的重要性愈发凸显。从金融交易的实时风险监控,到电商平台的用户行为分析,各行业都急需能快速处理海量数据的工具。Flink作为一款开源的分布式流处理框架,在这一领域崭露头…

Vue 封装http 请求

封装message 提示 Message.js import { ElMessage } from "element-plus";const showMessage (msg,callback,type)>{ElMessage({message: msg,type: type,duration: 3000,onClose:()>{if (callback) {callback();}}}); }const message {error: (msg,…

c++面试:类定义为什么可以放到头文件中

这个问题是刚了解预编译的时候产生的疑惑。 声明是指向编译器告知某个变量、函数或类的存在及其类型,但并不分配实际的存储空间。声明的主要目的是让编译器知道如何解析程序中的符号引用。定义不仅告诉编译器实体的存在,还会为该实体分配存储空间&#…