《代码整洁之道 Clean Code》学习笔记 Part 2 - 写出优雅的函数的10条建议
大师级程序员把系统当作故事来讲,而不是当作程序来写。
TLDR
- 短小(不超过 20 行、缩进不超过 2 层)
- 只做一件事
- 保持在同一抽象层级
- 用多态替代 switch
- 取个好的函数名
- 函数参数越少越好(尽量避免 3 个及以上参数)
- 无副作用、避免输出参数
- 分隔指令与询问
- 用异常替代错误码
- 不要重复(DRY: Don't Repeat Yourself)
1. 短小
这是函数的第一原则!作者几十年的的经验告诉我们:函数就应该短小,不接受反驳。
- 最好不要超过 20 行
- 缩进的层级不超过 2 层
- if/else/while 里面的代码应该只有一行(通常是一句函数调用):这样不但能保持函数短小,因为函数名本身具有说明性的名称,从而增加了文档上的价值
2. 只做一件事
参见:单一职责原则 SRP
函数的目的是把一个大的概念拆分成另一个抽象层级上的一系列步骤。
要判断函数是否只做了一件事,就看能否再拆出“改变抽象层级”的函数。
3. 每个函数一个抽象层级
什么是抽象层级?举个例子:
genHtml()位于较高抽象层pagePathName = PathParser.render(pagePath)位于中间抽象层.append("\n")位于较低的抽象层
如果一个函数中同时有这些不同层级的语句,那就违反了这个规则。
最佳实践:自顶向下读代码,每个函数后面跟着下一个抽象层级的函数。
4. 用多态替代 switch
switch 天生就要做 N 件事,很难写出短小的 switch 语句:
Money calculatePay(const Employee &e) {
switch (e.type) {
case COMMISIONED:
return calculateCommisionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw InvalidEmployeeType(e.type);
}
}
存在的问题:
- 违反了 SRP 单一职责原则:有好几个修改它的理由
- 违反了 OCP 开放关闭原则:每次添加新的员工类型都要修改这段代码
不仅如此,上述问题可能在多个地方重复出现:
bool isPayday(const Employee& e);
void deliverPay(const Employee& e, Money pay);
...
解决办法就是利用多态和抽象工厂,将 switch 封装到工厂内部,让 switch 只出现一次,用于创建多态对象:
// 抽象基类 Employee
class Employee {
public:
virtual bool isPayday() = 0;
virtual Money calculatePay() = 0;
virtual void deliverPay(Money pay) = 0;
};
// 用工厂封装 switch(只出现一次)
Employee *makeEmployee(...) {
switch (...) {
case COMMISIONED:
return CommissionedEmployee(...);
case HOURLY:
return HourlyEmployee(...);
case SALARIED:
return SalariedEmployee(...);
default:
throw InvalidEmployeeType(type);
}
}
isPayday、calculatePay、deliverPay 方法在具体子类中实现,对于调用 Employee 的代码来说,无需关心 Employee 的具体类型(面向接口编程)。和原来的实现相比:
- 增加新的员工类型时,需新增一个 Employee 的子类,实现 Employee 的抽象方法。此外,唯一需要修改的地方就是工厂中的 switch 语句,而其他使用 Employee 的代码无需改动。
- 函数参数减少一个:不再需要传入 Employee
5. 使用描述性名称
- 函数越短小,功能越集中,越容易取个好名字。
- 长而清晰的名字好过短而费解(以至于不得不额外加注释)的名字
- 追求好名字的过程有助于厘清设计思路,对代码进行改进重构
- 命名风格保持一致
6. 函数参数
函数参数越少越好,尽量避免 3 个及以上参数:
- 参数越多,代码越难以理解
- 多个参数的函数难以测试:因为很难覆盖所有的排列组合
- 避免使用 out 参数,应该直接返回输出内容(错误信息可以通过异常抛出或者返回
Result<T, Error>/std::optional<T>) - 避免 bool 参数,如果存在 bool 类型的参数,大概率是做了两件事,应该拆成两个函数
减少参数的方法:
- 把函数作为其中一个参数的成员:foo(a, b) --> a.foo(b)
- 把其中几个参数封装为类:makeCircle(double x, double y, double r) --> makeCircle(Point center, double r)
7. 无副作用
副作用会导致奇怪的时序性耦合及顺序依赖:
bool checkPassword(string username, string password) {
auto user = getUser(username);
string codedPhrase = user.getPhraseEncodedByPassword();
if(cryptographer.decrypt(codedPhrase, password) == "valid password") {
initializeSession(); // 副作用
return ture;
}
return false;
}
问题出在 initializeSession() 的调用:如果调用者只想核验密码而调用 checkPassword,就会意外重置当前 session。另外,这一副作用也导致了时序性耦合,即 checkPassword 只能在特定时候调用,在其他时刻调用则可能导致 session 数据丢失。如果一定要时序性耦合,至少应该在函数名中体现,如 checkPasswordAndInitializeSession,但这样还是违反了“只做一件事”的原则。
输出参数是另一种副作用。面向对象几乎不需要输出参数,如果函数需要修改某个状态,应该修改所属对象的状态。
8.分隔指令与询问
函数要么做什么事,要么回答什么事,不可以同时做两件事。应该避免这样的函数:
// 成功返回 true,属性不存在返回 false
bool set(string attr, string value);
这会导致这样的语句:
if(set("username","zijian"))
{
// 如果不看注释,很难知道 set 函数到底做了什么,返回值代表什么含义
}
即使将 set 修改为一个更贴切的名字 setAndCheckIfExists 也不会对可读性有多少提升。真正的解决方案是将指令与询问分隔开来:
if(attributeExists("username")) {
setAttribute("username", "zijian");
...
}
9. 使用异常替代错误码
指令式的函数返回错误码也违反了上一条“指令与询问分隔”的规则。
更讨厌的是,错误码会“强迫”调用者立即检查返回值,进而导致深层嵌套,难以阅读。你一定写过或见过类似的代码:
if (DeletePage(page) == E_OK) {
if (registry.DeleteReference(page.name) == E_OK) {
if (configKeys.DeleteKey(page.name.MakeKey() == E_OK)) {
logger.Log("page deleted");
} else {
logger.Log("configKey not deleted");
}
} else {
logger.Log("deleteReference from registry file failed");
}
} else {
logger.Log("delete failed");
return E_ERROR;
}
而如果使用异常替代错误码,就可以将错误处理和主要逻辑分离开:
try {
DeletePage(page);
registry.DeleteReference(page.name);
configKeys.DeleteKey(page.name.MakeKey();
} catch (const std::exception &e) {
logger.Log(e.what());
}
更进一步,try/catch 也应该和代码的正常流程分离:
void Delete(Page *page) {
try {
DeletePageAndAllReference(page);
} catch (const std::exception &e) {
LogError(e);
}
}
void DeletePageAndAllReference(Page *page) {
DeletePage(page);
registry.DeleteReference(page.name);
configKeys.DeleteKey(page.name.makeKey();
}
void LogError(const std::exception &e) {
logger.Log(e.what());
}
一个函数只做一件事,而错误处理本身就是一件事。换句话说,如果一个函数中有 try/catch,那么这个函数的第一个单词就应该是 try,catch 之后不应该再有其他代码,每个 try/catch 语句中,应该只有一个语句/函数调用。
这么做的好处:
- Delete() 函数只负责错误处理,阅读代码的时候大脑会“自动跳过”
- 主要逻辑函数 DeletePageAndAllReference() 可以完全不用考虑错误处理
使用异常替代错误码的第二个好处是完全遵循了 OCP 开放关闭原则。项目中经常见到 ErrorCode/ReturnCode/Result 这样的 enum 类型,很多的类都依赖它。每次修改时,所有依赖的代码都需要重新编译和部署,因此很多程序员宁愿复用现有的、不太精确的错误码(如 UNKNOWN_ERROR),也不愿增加一个更清晰、准确的错误码。而异常具有继承体系,可以通过增加新的子类来扩展错误,而完全不用担心影响旧的代码。
总结一下,用异常替代错误码的好处:
- 使用户能够将错误处理的代码从主要逻辑中分离出来,阅读代码时能够轻松跳过错误处理部分,将重点放在主要逻辑
- 遵循开放关闭原则(OCP),利用异常的继承体系,新的异常可以作为 exception 的新增子类,旧的代码不需要改动
️ 虽然异常有很多优点,但抛出异常会产生额外的开销(可能影响到确定性调度)。此外,编写异常安全的代码非常困难!因此一些对安全有特殊要求的应用会禁止使用异常,如汽车领域中很多对功能安全有要求的场合会禁止使用异常。
10. 不要重复(DRY: Don't Repeat Yourself)
重复是一切邪恶的根源。许多原则和规则都是为了消除重复:Codd 数据库范式、面向对象的继承、函数……
11. 结构化编程
之前的《架构整洁之道》中有介绍过,结构化编程是三个编程范式之一。所谓结构化编程,即每个函数、代码块都应该只有一个入口、一个出口。具体对编码的限制是:一个函数只有一个 return,循环中不能有 break/continue,绝对不能使用 goto。
作者认可结构化编程的目标和规范,但是!!!这些规则对于小函数帮助不大。如果函数足够短小,及早地 return/break/continue 可以使代码意图更清晰!而 goto 通常只有在大函数中才有作用,小函数中应该避免使用。
12. 如何写出这样的函数
即使作者 Uncle Bob 也不能一开始就写出这样的函数。
- 先写初稿,并配上单元测试
- 重构、优化:拆解函数、消除重复、修改名称
- 保持单元测试通过
《代码整洁之道 Clean Code》学习笔记 Part 2 - 写出优雅的函数的10条建议的更多相关文章
- 2015年第11本:代码整洁之道Clean Code
前一段时间一直在看英文小说,在读到<Before I fall>这本书时,读了40%多实在看不下去了,受不了美国人啰啰嗦嗦的写作风格,还是读IT专业书吧. 从5月9日开始看<代码整洁 ...
- 代码整洁之道Clean Code笔记
@ 目录 第 1 章 Clean Code 整洁代码(3星) ?为什么要整洁的代码 ?什么叫做整洁代码 第 2 章 Meaningful Names 有意义的命名(3星) 第 3 章 Function ...
- 代码整洁之道Clean Code 读后感After Reading
1.有意义的命名 名副其实,避免误导 做有意义的区分,简单明了2.函数 短小,职责单一 别重复自己3.注释 用代码来阐述 可怕的废话4.格式 垂直格式,垂直距离,空范围 横向格式,水平对齐,缩进5.错 ...
- 《代码整洁之道》(Clean Code)- 读书笔记
一.关于Bob大叔的Clean Code <代码整洁之道>主要讲述了一系列行之有效的整洁代码操作实践.软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关.这一点,无论是敏捷开发流派 ...
- 读《Clean Code 代码整洁之道》之感悟
盲目自信,自认为已经敲了几年代码,还看什么整洁之道啊.我那可爱的书架读懂了我的心思,很明事理的保护起来这本小可爱,未曾让它与我牵手 最近项目中的 bug 有点多,改动代码十分吃力,每看一行代码都带一句 ...
- Clean Code 代码整洁之道
军规:让营地比你来时更干净. 整洁代码 Leblanc : Later equals never. (勒布朗法则:稍后等于永不) 对代码的每次修改都影响到其他两三处代码. 修改无小事. 如同医生不能遵 ...
- 《代码整洁之道》ch1~ch4读书笔记 PB16110698 (~3.8 第一周)
<代码整洁之道>ch1~ch4读书笔记 <clean code>正如其书名所言,是一本关于整洁代码规范的“教科书”.作者在书中通过实例阐述了整洁代码带来的种种利处以及混乱代码 ...
- <读书笔记> 代码整洁之道
概述 1.本文档的内容主要来源于书籍<代码整洁之道>作者Robert C.Martin,属于读书笔记. 2.软件质量,不仅依赖于架构和项目管理,而且与代码质量紧密相关,本书提出一 ...
- 《代码整洁之道》ch5~ch9读书笔记 PB16110698(~3.15) 第二周
<代码整洁之道>ch5~ch9读书笔记 本周我阅读了本书的第5~9章节,进一步了解整洁代码需要注意的几个方面:格式.对象与数据结构.错误处理.边界测试.单元测试和类的规范.以下我将分别记录 ...
- <代码整洁之道>、<java与模式>、<head first设计模式>读书笔记集合
一.前言 几个月前的看书笔记 ...
随机推荐
- 成本阶问题:财务模块axcr004合计金额检核表第18行合计金额与明细差异过大问题处理?
财务模块axcr004合计金额检核表第18行合计金额与明细差异过大问题处理? 可能原因:生产开立工单时元件未建在生产料件BOM明细中,导致成本阶没有算到,需要手动更改成本阶. 公式: 处理办法:修改成 ...
- CCF CSP认证注册、报名、查询成绩、做模拟题等答疑
CCF CSP认证注册.报名.查询成绩.做模拟题等答疑 CCF CSP认证中心将考生在注册,或报名,或查询成绩,或历次真题练习时遇到的问题进行汇总,并给出解决方法,具体如下: 1.注册时,姓名可否随意 ...
- nginx配置解决跨域访问
场景:前后的分离项目,前端vue框架,打包后放在Tomcat里访问,端口是8080,后端服务端口8058.访问前端项目时,调用后端接口报跨域. 后端环境 正常访问端口8058 经过nginx配置(文末 ...
- 轻松合并Excel工作表:Java批量操作优化技巧
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 在Excel中设计表单时,我们经常需要对收集的信息进行统 ...
- .NET周刊【10月第2期 2023-10-08】
国内文章 起风了,NCC 云原生项目孵化计划 https://www.cnblogs.com/liuhaoyang/p/ncc-the-wind-rises.html 2016年,我和几位朋友发起了. ...
- 高效技巧揭秘:Java轻松批量插入或删除Excel行列操作
摘要:本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 在职场生活中,对Excel工作表的行和列进行操作是非常普遍的需求 ...
- 机器学习|K邻近(K Nearest-Neighbours)
本文从概念.原理.距离函数.K 值选择.K 值影响..优缺点.应用几方面详细讲述了 KNN 算法 K 近临(K Nearest-Neighbours) 一种简单的监督学习算法,惰性学习算法,在技术上并 ...
- 20.3 OpenSSL 对称AES加解密算法
AES算法是一种对称加密算法,全称为高级加密标准(Advanced Encryption Standard).它是一种分组密码,以128比特为一个分组进行加密,其密钥长度可以是128比特.192比特或 ...
- SpringBoot数据响应、分层解耦、三层架构
响应数据 @ResponseBody 类型:方法注解.类注解 位置:Controller方法.类上 作用:将方法返回值直接响应,如果返回值类型是 实体对象/集合 ,将会转换为json格式响应 说明:@ ...
- .NET的各种对象在内存中如何布局[博文汇总]
在过去一段时间里,我陆陆续续写一些关于.NET对象类型布局的文章,其中包括值类型和引用类型的内存布局.字符串对象和数组的内存布局等,这里作一个简单的汇总. [1] 如何计算一个实例占用多少内存? 我们 ...