Clean Code Chapter 1-8

  • 第一章 整洁代码
    • 1. 为什么需要代码?
    • 2. 混乱的代码
    • 3. 什么是整洁代码
  • 第二章 有意义的命名
    • 1. 名副其实
    • 2. 避免误导
    • 3. 做有意义的区分
    • 4. 使用读得出来的名称
    • 5. 使用可搜索的名称
    • 6. 避免使用编码
    • 7. 避免思维映射
    • 8. 类名
    • 9. 方法名
    • 10. 每个概念对应一个词
    • 11. 别用双关语
    • 12. 使用解决方案领域名称
    • 13. 添加有意义的语境
    • 14. 不添加没用的语境
  • 第三章 函数
    • 1. 短小
    • 2. 只做一件事
    • 3. 每个函数一个抽象层级
    • 4. switch 语句(还要再查查资料)
    • 5. 使用描述性的名称
    • 6. 函数参数
      • 一元函数的普遍形式
      • 标识参数
      • 参数对象
      • 参数列表
    • 7. 无副作用
      • 输出参数
    • 8. 分隔指令与询问
    • 9. 使用异常替代返回错误码
      • 抽离 Try/Catch 代码块
      • 错误处理就是一件事
    • 10. 别重复自己
    • 11. 结构化编程
  • 第四章 注释
    • 1. 注释不能美化代码
    • 2. 用代码来阐述
    • 3. 好注释
  • 第五章 格式
    • 1. 垂直格式
    • 2. 横向格式
  • 第六章 对象和数据结构
    • 1. 数据抽象
    • 2. 数据、对象的反对称性
    • 3. Law of Demeter
    • 4. 数据传送对象
  • 第七章 错误处理
  • 第八章 边界

第一章 整洁代码

1. 为什么需要代码?

模型和需求固然重要,但是代码呈现了需求的细节,在某些层面上,这些细节无法被忽略,必须被明确。编程就是将需求明确到机器可以执行的细节程度,而这种规约制度就是代码

代码是我们最终用来表达需求的语言。

2. 混乱的代码

作者认为糟糕的代码会使后续的修改和维护成本增加,也许项目初期进展迅速,但随后对代码每次修改都会影响到其他代码,造成更加混乱的代码。

勒布朗法则:稍后等于永不(Later equals never)

赶上期限的唯一方法:始终保持代码整洁。

3. 什么是整洁代码

Bjarne Stroustrup(C++语言发明者):代码逻辑直截了当,尽量减少依赖关系,使代码便于维护;依据分层方法完善错误处理代码;性能最优。整洁的代码只做好一件事。

Grady Booch:整洁的代码简单直接,从不隐藏设计者的意图。

Dave Thomas:整洁的代码可由其他开发者阅读和增补,应有单元测试和验收测试;使用有意义的命名;只提供一种做一件事的途径;尽量少的依赖关系,且明确定义和提供清晰、少量的API;代码应通过字面表达含义。

作者认为Dave关于代码易修改性的观点,虽然代码便于其他人增补也是重要的一点,但是也不能过分强调其重要性,易读的代码和易修改的代码之间是有区别的。

Ron Jeffries:简单代码依其重要性:

  • 能通过所有测试;
  • 没有重复代码;
  • 体现系统中全部设计理念;
  • 包括尽量少的实体(如类、方法、函数等)

Ron认为以上最重要的是没有重复代码,因为如果一段代码反复出现,就说明某种想法在代码中没有很好的表达出来。消除重复和提高表达力是改进代码的重要方法。

第二章 有意义的命名

作者列出了命名的规则:名副其实、有意义的区分、使用读得出来的名称、避免使用编码、避免思维映射、每个概念对应一词、别用双关语、使用解决方案领域名称、使用源自所涉问题领域的名称、添加有意义的语境、不要添加没用的语境。

1. 名副其实

变量、函数或类的名称应该能告诉你它为什么存在,做什么事情,应该怎么用,如果名称需要注释补充,就不算是名副其实。

命名时尽量选择体现本意,指明计量对象和计量单位的名称:

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

2. 避免误导

必须避免留下掩藏代码本意的错误线索,避免使用与本意相悖的词。

如 hp、aix和 sco 不应该作为变量名,它们是 UNIX 平台或类 UNIX 平台的专有名称。避免在名称中写出容器的类型名,如 accountList 表示一组账号,可能会引起错误判断,用 accountGroup 或 bunchOfAccounts、accounts 都会更高。

避免使用相似度较高的名称,如 XYZControllerForEfficientHandlingOfStrings 和 XYZControllerForEfficientStorageOfStrings。

避免使用 0、1、o、l 这一类比较容易混淆的字母作为变量名。

3. 做有意义的区分

不要因为统一作用域内两样不同的东西不能重名,就随手改掉其中一个的名称。

例如 class 已经用了,就给另一个变量命名为 kclass。

或者是以数字序列命名,a1,a2,…,aN。这种命名没有提供正确信息。

又或者有一个 Product 类,而另一个 ProductInfo 类和 PriductData 类的名称意义就没有什么区别。

废话都是冗余的,例如 Variable 不应该出现在变量名中,Table 不应该出现在表名中,NameString 和 Name 没有区别,Customer 类和 CustomerObject 类没有区分度。

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

上述的三种方法名不能区分它们的用途。

4. 使用读得出来的名称

使用恰当的英语单词,要避免俗话或俚语。如 DeleteItems 要比 HolyHandGrenade 更好,别用 whack() 表示 kill(),别用 eatMyShorts() 表示 abort()。

言到意到,意到言到。

5. 使用可搜索的名称

单字母名称仅用于短方法的本地变量,名称长短应与作用域大小相对应。如果变量或常量可能在代码中多处使用,应赋其便于搜素的名称。如 MAX_ClASSES_PER_STUDENT 就要比数字 7 容易搜索。

6. 避免使用编码

  • 不必在名称中体现类型;
  • 不需要用 m_ 前缀表名成员变量;
  • 如果接口和实现需要有一个编码,可以选择对实现名称编码。如 ShapeFactory 和 ShapeFactoryImp,为用户提供的接口名称要简介。

7. 避免思维映射

不应让读者将你的名称翻译为他们熟知的名称。比如在循环计数中常用 i、j、k 来命名,而在其他地方应该避免使用单字母名称。

8. 类名

类名和对象名应该是名词或名词短语,如 Customer、WikiPage、Account 和 AddressParser。避免使用 Manger、Processor、Data 等。

9. 方法名

方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。属性访问器、修改器和断言应该根据其值命名,加上 get、set 和 is 前缀。

string name = employee.getName();
customer.setName("Mike");
if (paycheck.isPosted()) ...

10. 每个概念对应一个词

给每一个抽象概念选择一个词

11. 别用双关语

避免同一单词用在不同目的,同一术语用在不同概念。比如在多个类中都有 add 方法,该方法通过增加或链接两个现存值来获得新值。如果要写一个新类,类中的方法要把单个参数放在群集中没如果把这个方法叫做 add 的话就与其他类的 add 方法保持一致了,但是实际表达的意义却不同。此时应该用 insert 或者 append 来命名这个新类中的方法更合适。

12. 使用解决方案领域名称

尽量使用计算机科学术语,如果不能用熟悉的术语命名,就采用所涉问题领域的名称。

13. 添加有意义的语境

用良好命名的类、函数或名称空间来放置名称,给读者提供语境。

如添加前缀 add人FirstName、addrLastName、addrState 等提供语境,让读者明白这是表达地址。更好地方案是创建 Address 的类。

14. 不添加没用的语境

只要短名称足够清楚,就要比长名称好,不要给名称添加不必要的语境。

对于 Address 类名称就足够好,如果要与 MAC 地址、端口地址和 Web 地址区别,会考虑使用 PostalAddress、MAC 或者 URI 更为精确。

第三章 函数

1. 短小

函数的第一规则就是要短小,每个函数以 20 行封顶最佳。

最好能在代码块内调用函数,调用函数的名称具有一定的说明性。函数的缩进层级不应该多于一层或两层。

2. 只做一件事

函数应该做一件事。函数只做能表达函数名称这一抽象层上的事,编写函数就是为了把大一些的概念拆分为另一抽象层上的一系列步骤。

要判断函数是否不止做了一件事,就是看是否能再拆出一个函数。但是要注意拆出的函数不仅只是单纯地重新诠释其实现,要在另一个抽象层上实现。

3. 每个函数一个抽象层级

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。

自顶向下规则:让代码拥有自顶向下的阅读顺序,每个函数后面都跟着位于下一抽象层级的函数。

4. switch 语句(还要再查查资料)

switch 语句如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到。具体情况具体分析。

5. 使用描述性的名称

沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”

长而具有描述性的名称,要比短而令人费解的名称好。选择描述性的名称能帮你理清模块的设计思路。

命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。

6. 函数参数

应该避免使函数参数超过三个以上,三参数函数也要尽量避免。

一元函数的普遍形式

一种是由输入和输出参数的形式。如boolean fileExists("MyFile")。也可能是操作参数,将其转换为其他类型,如InputStream fileOpen("MyFile")把 String 类型的文件名转换为 InputStream 类型的返回值。

另一种是单参数函数形式——事件。这种形式有输入参数而没有输出参数,程序将函数看作是一个事件,使用该参数修改系统状态。例如 void passwordAttemptFailedNtimes(int attempts)

要尽量避免编写除者两种形式意外的一元函数。例如函数对输入参数进行转换操作,转换结果就应该体现为返回值,而不是使用输出参数。即StringBuffer transform(StringBuffer in)要比void transform(StringBuffer out)强。

标识参数

不要将布尔值传入函数参数。会让函数复杂,不止做一件事。

参数对象

如果函数需要两个、三个或三个以上参数,就说明其中一些参数应该封装成类。

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

参数列表

如果想向函数传入数量可变的参数,可以使用参数列表

#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);

7. 无副作用

输出参数

参数多数会被认为是函数的输入,应避免使用输出参数。如果函数必须修改某种状态,就修改所属对象的状态。

8. 分隔指令与询问

函数要么做什么事,要么回答什么事;函数应该修改某对象的状态,或者返回该对象的有关信息,不能同时使用。

if (set("username", "unclebob"))...

这样设置容易让人混淆,应该吧指令和询问分隔开:

if (attributeExists("username")) {setAttribute("username", "unclebob");
}

9. 使用异常替代返回错误码

从指令式函数返回错误码稍微违反了指令与询问的规则,在 if 语句判断中把指令当做表达式使用,又会导致更深层次的嵌套结构,当返回错误吗时,就是在要求调用者立刻解决处理错误。

可以使用异常替代返回错误码,错误处理代码能从主路径代码中分离出来:

try {deletePage(page);registry.deleteReference(page.name);configKeys.deleteKey(page.name.makeKey());
}
catch (Ececption e) {logger.log(e.getMessage());
}

抽离 Try/Catch 代码块

把错误处理与正常流程混为一谈,会搞乱代码结构。最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。

void delete(Page page) {try {deletePageAndAllReferences(page);}catch (Exception e) {logError(e);}
}
void deletePageAndAllReferences(Page page) {deletePage(page);registry.deleteReference(page.name);configKeys.deleteKey(page.name.makeKey());
}void logError(Exception e) {logger.log(e.getMessage());
}

上述中 delete 函数只于错误处理有关,deletePageAndAllReferences 函数只于完全删除一个 page 有关。

错误处理就是一件事

函数只应该做一件事,错误处理就是一件事。

10. 别重复自己

许多原则和实践规则都是为控制和消除重复而创建。如面向对象编程将代码集中到基类,从而避免了冗余。

11. 结构化编程

Dijkstra 的结构化编程规则:每个函数、函数的每个代码块都应该有一个入口、一个出口。每个函数都只该有一个 return 语句,循环中不能有 break 或 continue 语句,而且永远不能有任何 goto 语句。

作者认为在大函数中这些规则才会有明显的好处,只要我们保持函数短小,偶尔出现的 return、break 或 continue 语句没有坏处,甚至比单入单出原则更具表达力。但 goto 语句应该尽量避免使用。

第四章 注释

注释是在代码表达意图失败时使用,我们应该尽量使用代码本身来表达含义。程序员应当负责将注释保持在可维护、有关联、精确的高度,作者认同这一观点,但他认为更许愿更应该把注意力集中在写清楚代码上,保证无需编写注释。不准确的注释比没有注释要坏的多。

1. 注释不能美化代码

写注释的常见原因之一就是糟糕的代码的存在,所以与其为糟糕代码写好注释,不如多花时间清理糟糕的代码。

2. 用代码来阐述

有时候只需要创建一个描述与注释所言的函数就可以来解释功能:

if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))...
if (employee.isEligibleForFullBenefits())...

3. 好注释

有些注释是必须的,但好的注释是永远是不去写的注释。

  • 法律信息,公司代码规范要求编写与法律有关的注释;
  • 提供信息的注释
  • 对意图的解释,注释不经提供实现的信息,而且还提供某个决定后面的意图;
  • 阐释,如果参数是某个标准库的一部分,或者你不能修改的代码,注释其含义就有必要;
  • 警示,用于⚠️其他程序员会出现某种后果的注释,使用解释性字符串@Ignore
  • TODO 注释,但是要注意定期查看并删除不需要的代码;

第五章 格式

1. 垂直格式

  • 大多数的文件在 200 行,最长 500 行。源文件就像报纸文章一样的可读性

    • 源文件的名称应该简单且一目了然,名称本身应该告诉我们是否在在正确的模块中。
    • 源文件最顶部给出高层次概念和算法,细节应该向下渐次展开,直到源文件中最底层的函数和细节。
  • 每行展现一个表达式或一个句子,每组代码展示一条完整地思路,每组代码之间通过空白行隔开。
  • 空白行隔开概念,而紧密相关的代码就应该相互靠近。
  • 对于关系密切、放置在同一源文件的概念,它们之间的间隔不应过大。
    • 变量声明应尽可能靠近其使用位置。函数本地变量应该出现在函数顶部,循环的控制变量应该在循环语句中声明,在较长的函数中,变量也可在某个代码块的顶部、循环之前声明。
    • 实体变量应在类的顶部声明。C++中通常采用“剪刀原则”将实体变量都放在底部,在 Java 中惯例放在类的顶部。
    • 相关函数应该放在一起,一般将调用者放在被调用者上面。
    • 概念相关的代码应该放在一起,相关性越强,距离越近。

2. 横向格式

  • 一行代码中 45 个字符左右的宽度最适合。应该尽力保持代码行短小,作者推荐上限 120 个字符。

    • 使用空格符将两个要素分隔开。
    • 对声明和赋值不对齐。
    • 空范围,有时 while 或 for 语句的语句体为空(尽量避免这种情况)。如果无法避免,就确保空范围体的缩进,否则很容易诱导别人。
    while (dis.read(buf, 0, readBufferSize) != -1);
    

第六章 对象和数据结构

1. 数据抽象

数据应该以抽象形态表述,而不要直接暴露数据细节。隐藏实现并不是说简单的在变量之上放一个函数层,并不只是使用接口、赋值器或取值器就行了,而是要以最好的方式呈现对象包含的数据。

2. 数据、对象的反对称性

  • 对象与数据结构的差异:对象把数据隐藏在抽象之后,暴露操作数据的函数;数据结构暴露其数据,没有提供有意义的函数。
  • 对象与数据结构之间的二分原理:过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。

3. Law of Demeter

The Law of Demeter 认为模块不应了解所操作对象的内部情形。类 C 的方法 f 只应该调用以下对象的方法:

  • C
  • 由 f 创建的对象
  • 作为参数传递给 f 的对象
  • 由 C 的实体变量持有的对象

方法不应该调用任何函数返回的对象的方法。如:

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

4. 数据传送对象

最精练的数据结构时一个只有公共变量、没有函数的类,称为数据传送对象 DTO(Data Transfer Objects)。DTO 在数据库通信或解析套接字传递的消息之类的场景很有用。

Active Record 是一种特殊的 DTO 形式,拥有公共变量的数据结构通常会拥有 save 或 find 类似的方法。Active Record 一般是对数据库表或其他数据源的直接翻译。不要把这一类数据结构当对象来用,会导致数据结构和对象的混杂。

解决方案:把 Active Record 当数据结构,并创建包含业务规则、隐藏内部数据的独立对象。

第七章 错误处理

  • C 语言中不支持异常处理,通常的手段是设置错误标识(使用 goto 语句),或者返回调用者检查的错误码。

  • 对于抛出的每一个异常,都应当提供足够的环境说明,以便判断错误的来源。

  • 在方法中应该尽可能避免传递 null 值,如果传入了 null 值,可以使用断言来解决异常。

    assert(ptr != NULL);
    

第八章 边界

在使用第三方程序包或开源代码时,我们应当将其干净利落地整合进自己的代码中。

  • 使用第三方代码

    • 第三方代码通常具有普适性,而使用者需要满足特定需求的接口。比如使用者需要构造一个 Map 对象在程序中传递,但不希望 Map 对象的接收者都能删除映射图中的任何东西,然而 Map 其中的一个接口就是 clear(),这就使任何接收者都可能删除其中的内容。

    • 构造一个包容 Sensor 类对象的 Map 映射图:

      Map sensors = new HashMap();
      Sensor s = (Sensor)sensors.get(sensorId);
      

      代码调用端从 Map 中取得对象并将其转换为正确类型,并非整洁的代码,也没有说明用途。

      • 解决方法一:泛型

        Map<Sensor> sensors = new HashMap<Sensor>();
        Sensor s = sensors.get(sensorId);
        

        提高了可读性,但在系统中不断传递 Map<Sensor> 的实体,当 Map 的接口被修改后,需要对代码进行大量的修改。

      • 解决方法二:封装

        public class Sensors {private Map sensors = new HashMap();public Sensor getById(String id) {return (Sensor) sensors.get(id);}
        }
        

        将边界上的接口(Map)隐藏,在实现的细节中我们才关心是否使用了泛型,转换和类型管理都是在类内处理了,也就减小了因接口修改而产生修改代码的工作量。

    • 建议不要将 Map 或边界上其他接口在系统中传递。如果使用类似 Map 的边界接口,就把它保留在类或近亲类中,避免从公共 API 中返回边界接口,或将边界接口作为参数传递给公共 API。

  • 浏览和学习边界。为了利用第三方程序包时,为要使用的第三方代码编写测试时最快的学习方法。

    • 学习性测试可以帮助我们增进对 API 的理解。
    • 学习性测试确保第三方程序包按照我们想要的方式工作。
  • 使用尚不存在的代码

    • 对于自己尚不理解而且还未开发出的 API,我们可以先定义自己目前使用的接口,之后实现好 API 的细节后再将其跨接在自己定义的接口中。
  • 整洁的边界

    • 在使用第三方代码时需要小心使用,确保未来修改不至于代价太大。
    • 边界上的代码需要清晰的分割和定义期望的测试,避免自己的代码过多了解第三方代码的特定信息。
    • 通过代码中少数几处引用第三方边界接口的位置来管理第三方边界。如封装 Map,或使用 ADAPTER 模式将自己的接口转换为第三方提供的接口。

Clean Code 代码整洁之道笔记(1-8 章)相关推荐

  1. Clean Code 代码整洁之道

    一直想深入看看<CleanCode 代码整洁之道>,增强代码整洁性.看到此文,略有启发,转载以敬之. 作者:JobsandCzj  来源:CSDN  原文:https://blog.csd ...

  2. 《代码整洁之道》—第1章1.1节要有代码

    本节书摘来自异步社区<代码整洁之道>一书中的第1章1.1节要有代码,作者[美]Robert C. Martin,更多章节内容可以访问云栖社区"异步社区"公众号查看. 第 ...

  3. 《代码整洁之道》—第1章1.4节思想流派

    本节书摘来自异步社区<代码整洁之道>一书中的第1章1.4节思想流派,作者[美]Robert C. Martin,更多章节内容可以访问云栖社区"异步社区"公众号查看. 1 ...

  4. 代码整洁之道第十二章-迭进

    通过迭进设计达到整洁的目的,本章主要介绍了四条简单的规则,用以帮助你更轻易的实现代码的良好的设计已经更轻松的应用SRP和DIP等原则,这四条规则是 运行所有测试 不可重复 表达程序员的意图 尽可能减少 ...

  5. 【代码整洁之道】第六章:对象和数据结构

    第六章 对象和数据结构 中国话: 将类中的成员变量设为私有private,就可以对外部隐藏类的实现细节.当修改类中方法的具体实现时,不会影响外部对类中方法的调用. 但是,有些码农总喜欢给类的所有成员变 ...

  6. [学习笔记]《代码整洁之道》(八)

    [学习笔记] <代码整洁之道>- 第9章 单元测试 TDD 三定律 谁都知道TDD要求我们在编写生产代码之前先编写单元测试. 定律一:在编写不能通过的测试单元前,不可以编写生产代码. 定律 ...

  7. C#代码整洁之道读后总结与感想

    1. 基本信息 C#代码整洁之道:代码重构与性能提升 ,英文名为Clean Code in C#. 作者:[英] 詹森·奥尔斯(Jason Alls) 著,刘夏 译 机械工业出版社,2022年4月出版 ...

  8. 《代码整洁之道》(Clean Code)- 读书笔记

    一.关于Bob大叔的Clean Code <代码整洁之道>主要讲述了一系列行之有效的整洁代码操作实践.软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关.这一点,无论是敏捷开发流派 ...

  9. 代码整洁之道(Clean Code)- 读书笔记

    Sorry, 许久未更新文章了,主要因为刚刚换了一家新公司,忙于组建团队(建设.招聘.流程.框架等)与熟悉公司业务,还有领导给的其他工作等等,实在是没有时间更新了.最近在和团队分享Bob大叔的< ...

最新文章

  1. 管理的实践-彼得.德鲁克
  2. linux 运行msi文件是什么意思,查看Msi文件内容
  3. 【KubeVela 官方文档翻译】,欢迎大家踊跃参与
  4. matlab读取文件夹下所有文件的字符串,MATLAB读取文件夹下所有文件的文件名并读取数据...
  5. python编辑邮件格式_python发送邮件模板
  6. 【UML关系(泛化、实现、依赖、关联(聚合,组合))】
  7. Ubuntu 16.04中zabbix显示 :Zabbix server is not running:te information displayed may not be current.
  8. linux已开机时间,Linux查看系统开机时间
  9. Cannot find class ‘org.apache.hudi.hadoop.HoodieParquetInputFormat‘
  10. 蓝队应对攻击的常用策略二
  11. webstorm主题设置
  12. [状压dp] 炮兵阵地(状压dp)
  13. html在线围棋对战,闲情奕趣(基于html5的围棋应用)
  14. 内构函数java_图灵学院笔记-java虚拟机底层原理
  15. uniapp实现版本更新
  16. java 数组总结(赋值,反转,添加,查找)
  17. 2021 12月CSP认证心得
  18. Google Code Search
  19. ESP32 ADF windows开发环境搭建 适配ADF到ESP32A1S
  20. NJFU软件需求分析试卷

热门文章

  1. Windows Mobile 开发总结
  2. 看故事自学Android安卓开发,Android安卓王国历险记第1集:Android问世
  3. 文科生学Python,为了什么?
  4. IPFS中国区教父周欢膨胀了?怒砸千万只为推动IPFS落地应用
  5. 全世界B站API接口大合集!
  6. 多表连接查询详细解析(详细版)
  7. Golang的锁机制
  8. Centos7开启telnet登录
  9. 在VS2019中配置 OpenCV4.5.5教程
  10. MATLAB函数——newff()