内容简介:《代码整洁之道》主要讲述了一系列行之有效的整洁代码操作实践。软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。这本书的阅读对象为一切有志于改善代码质量的程序员,书中介绍的规则均来自作者Bob大叔多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。封面图片:
《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
《代码整洁之道》主要讲述了一系列行之有效的整洁代码操作实践。软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。这本书的阅读对象为一切有志于改善代码质量的程序员,书中介绍的规则均来自作者Bob大叔多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。
封面图片:
上面这张图是M104:草帽星系,其核心是一个质量超大的黑洞,有100万个太阳那么重,环绕着M104的光环就像一顶墨西哥草帽,仿佛经历了大爆炸之后碎片四溅的产物。联系到我们所经历过的没由整洁代码风格各异不可维护的软件项目,其实当你接手时之前的代码都是一个个的黑洞,存在着某天会定时爆发的风险,而当它真正爆发时,接手这个项目的所有人都会因此遭殃。
其次,再说说副标题:“ 细节之中自有天地,整洁成就卓越代码 ”。本书讲述的就是一个又一个的细节之处,不好的处理和好的处理都一一道来,让我们形成整洁的规范。
Robert C. Martin,(Bob大叔)自1970年进入软件行业,从1990年起成为国际软件咨询师。是软件工程领域的大师级人物,是《敏捷软件开发:原则、模式与实践》、《敏捷软件开发:原则、模式与实践(C#版)》(邮电)、《极限编程实践》(邮电)等国内引进的畅销书的作者,其中第一本原著荣获美国《软件开发》第13届震撼(Jolt)大奖,Martin的敏捷系列书是软件工程界的权威书籍。
Clean Code:
代码逻辑应该直接了当,叫缺陷难以隐藏;
尽量减少依赖关系,使之便于维护;
依据某种分层战略完善错误处理代码;
性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来;
整洁的代码只做好一件事;
有单元测试和验收测试;
有意义的命名;
尽量“少”;
两条重要原则:
尽量让代码易读,开发时减少读的时间。
童子军军规:“让营地比你来时更干净”。
1. 一切代码与注释都是有实际意义的,没有冗余,整洁干净
2. 代码能通过所有测试,运行高效
3. 命名名副其实,区分清晰,意义明了,尽量做到看名字就能知道你的意图
4. 代码逻辑尽量直白,简单
5. 每个方法只做一件事,功能明确且单一,方法间层次分明
6. 每个类职责尽量单一,高内聚,类与类之间低耦合
7. 测试覆盖面广,每个测试用例基本只测一个点
8. 测试代码的要求与业务代码一样高.
第一章 为什么要整洁代码
1、代码永不消失
代码就是衔接 人脑理解需求的含糊性 和 机器指令的精确性 的桥梁。哪怕未来会有对现在高级编程语言的再一次抽象——但这个抽象规范自身仍旧是代码。
所以既然代码会一直存在下去,且自己都干了 程序员 这一行了,就好好的对待它吧。
2、读远比写多
当你录下你平时的编码的过程,回放时,你会发现读代码所花的时间远比写的多,甚至超过 10:1。所以整洁的代码会增加 可读性 。
3、对抗拖延症—— 稍后等于永不
糟糕的代码会导致项目的 难以维护 。而当你正在写糟糕的代码的时候,心里却圣洁的想:“有朝一日再回来整理”。但现实是残酷的,正如 勒布朗(LeBlanc)法则
:稍后等于永不(Later equals never)
4、精益求精
写代码就跟写文章一样,先自由发挥,再细节打磨。 追求完美 。
大师级程序员把系统当作故事来讲,而不是当作程序来写。
第二章 命名
1、名副其实
(×)图表:mapTable
(√)图表:chart
推荐一个根据中文意思帮你起英文变量名的网址: https://unbug.github.io/codelf/
2、避免误导
(1)不要用专有名词
const var = 0;
(2)避免细微之处有不同
XYZControllerForEfficientKeepingOfStrings
XYZControllerForEfficientHoldingOfStrings
(3)避免废话
如果是一个数组类型,就没必要叫 ProductArray
如果返回值就是数据,就没必要叫 ProductData
(4)便于搜索
(×)
if ( team.length === 3 )
(√)
const MAX_NUM_OF_TEAM = 3 ;
……if ( team.length === MAX_NUM_OF_TEAM )
MAX_NUM_OF_TEAM 比 3 好检索.
早期流行一种 匈牙利语标记法
:
如 arru8NumberList 的前缀 "arru8" 表示变量是一个无符号8位整型数组;
3、避免思维映射
类名、参数名用名词:
member
leader
方法名用动词:
getTeam
根据 Javabean 的标准,方法名可以加上 get set is 前缀
例如循环中的 i、j、k
等单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。最好用 filter、map
等方法代替 for循环.
4、每个概念用一个词
get fetch 选一个,不要混着用.
5、添加有意义的语境
如果你要记录 member 的详细住址,会设置了如下几个变量:
(×)
addrStreet
addrHouseNumber
addrCity
addrState
(√)
new Address {
street,
houseNumber,
city,
state
}
第三章 函数
-
短小
if、else、while 语句内的代码块应该只有一行,该行大抵是一个函数调用语句。
这样的函数不仅能保持短小,而且调用的函数具有说明性的名称,从而增加了文档上的价值。
所以函数的缩进不能多于两层。
-
只做一件事
写函数是为了把大一些的概念(换言之,函数名称)拆分为另一抽象层上的一系列步骤。
判断函数是否不止做了一件事,还有一个办法就是看是否还能再拆出一个函数。
-
每个函数一个抽象层级
要让代码有自顶向下的阅读顺序,向下规则:每个函数后面跟着下一抽象层级的函数。
-
Switch语句
问题:太长,做了不止一件事,违反单一职责原则,违反开闭原则,到处存在类似结构的函数。
解决方案:把
switch
语句放在工厂类,使用接口多态的接受派遣。
-
使用描述性的名称
函数越短小,功能越集中,越便于取个好名字。
-
函数参数
-
参数越少越好,参数超过三个时,排序、琢磨、忽略的问题都会加倍体现。
-
一元函数的普遍形式:A.操作参数,转换,返回。B.传入event事件。
-
参数过多,最好先封装成类再传入。
-
避免使用标识参数,传入true/false,明显违反“只做一件事”的原则。
-
避免使用输出参数,如果要修改对象的状态,要调用对象自己的函数:
appendFooter(report);
应该改成
report.appendFooter();
-
分隔指令与询问
public boolean set(String attribute, String value);
该函数设置某个属性的值,如果设置成功返回true,如果不存在这个属性返回false。
就会导致以下语句出现:
if (set("userName", "Leo")) {...}
应该改成
if (attributeExists("userName")) { set("userName", "Leo") }
-
使用异常替代返回错误码
从指令式函数返回Error Code,轻微违反了指令与询问分隔的原则,而且导致更深层次的嵌套结构。使用异常,错误处理代码就能从主路径代码中分离出来,得到简化。
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 failed"); } } else { logger.log("delete failed"); return E_ERROR; }
改成
try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch (Exception e) { logger.log(e.getMessage()); }
try/catch 代码块违反了只做一件事的原则,应该把try和catch代码块主体部分抽出来,另外形成函数
public void delete(Page page) { try { deletePageAndAllReferrence(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferrence(Page page) { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
-
别重复自己
-
结构化编程
只要函数保持短小,循环偶尔出现 return, break, continue没有问题,避免使用
goto
。
-
函数修改的策略
对于冗长复杂的函数,先加 单元测试覆盖每行丑陋的代码,然后
分解函数、修改名称、消除重复
,同时保持单元测试通过。
小结
函数是动词,类是名称,编程艺术是语言设计的艺术。大师级程序员把系统当成故事来讲,而不是当作程序来写。
(ref:https://www.jianshu.com/p/822549096a82)
第四章 注释
尽量用代码表达,而不是用注释
就像上文提到的用详尽的函数名代替注释,或者:
//代码过于追求简洁而导致这里要加详细的注释 if( smodule.getDependSubsystems().contains(subSysMod.getSubSytem()) )
还不如这里做拆分,取易懂的变量名方便理解,就可以不用加注释或者少加
ArrayList moduleDependees = smodule.getDependSubsystems(); String ourSubSystem = subSysMod.getSubSystem(); if( moduleDependees.contains(ourSubSystem) )
原因是:注释存在的越久,随着代码的不断迭代,会离代码的距离越来越远,这个时候好的做法是同时维护代码 + 注释,而注释越多越复杂, 维护的成本 自然就上升了。
注释不能美化糟糕的代码,尽量去优化代码
好注释
(1)法律信息、许可证、版权、著作权、外链文档的 url
(2)对意图的解释
(3)警示
(4)TODO 注释
坏注释
(1)只有自己看得懂
(2)多余的注释
a、不能提供比代码更多的信息
b、读的时间比直接看代码还久
(3)歧义性
(4)循规蹈矩的注释
例如用第三方 工具 生成的注释,很多都是累赘的废话
(5)日志式、署名式注释
(×)
// write by colin// fix #201 bug
(√)
交给 git 记录
(6)慎用位置标记
// **********************
及时清理不需要的注释
(1)越堆越多
(2)导致以后因看不懂而不敢删
第五章 格式
向报纸学习
(1)从上往下读,从左往右读
(2)源文件在最顶端给出高层次的概念和算法,细节往下逐次展开,直到最底层的函数和细节。
垂直格式
(1)善用空白行:人会更容易将目光聚焦到空白行之后的那一行
(2)函数:调用者放在被调用者上面
横向格式
(1)缩进
(2)IDE 中不会出现横向滚动条
第六章 对象和数据结构
-
数据抽象
隐藏实现关乎抽象! 类并不简单的用getter、setter将其变量推向外间,
而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
以抽象形态表述数据。
//具象点 public class Point { public double x; public double y; } //抽象点 public interface Point { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); void setPolar(double r, double theta); }
//具象机动车 public interface Vehicle { double getFuelTankCapacityInGallons(); double getGallonsOfGasoline(); } //抽象机动车 public interface Vehicle { double getPercentFuelRemaining(); }
-
数据、对象的反对称性
-
对象把数据隐藏于抽象之后,暴露操作数据的函数。
数据结构暴露其数据,没有提供有意义的函数。
-
对象与数据结构之间的二分原理:
过程式代码 (使用数据结构)便于在不改动数据结构的前提下添加新函数,
面向对象代码 便于在不改动既有函数的前提下添加新类。
反过来说,
过程式代码 难以添加新数据结构,因为必须修改所有函数,
面向对象代码 难以添加新函数,因为必须修改所有类。
过程式代码
面向对象多态代码
-
德墨忒尔定律(迪米特法则,Law Of Demeter)
也叫做“最少了解原理”,模块不应该了解它所操作对象的内部情形。
C类的函数f()只能调用以下对象的方法:
-
C类的对象
-
f()创建的对象
-
通过参数传入的对象
-
C类的实体变量对象
另一种解释: 只暴露应该暴露的接口方法,只依赖需要依赖的对象 。
law of demeter sample.PNG
System应该只暴露close()的接口方法,而不该暴露close()内部的细节,
Person应该只依赖Container(硬件设备容器)的接口,而不该直接依赖System(操作系统)。
这样做也符合 依赖倒置原则 ,也就是 面向接口编程 。
火车失事
下列代码应该切分成三行。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
是否违反德墨忒尔定律,取决于ctxt、options、scratchDir、absolutePath是对象还是数据结构。
如果是对象,应该隐藏内部结构。
如果是数据结构,则需要暴露内部结构,不算违反德墨忒尔定律。
混杂
尽量避免混合结构,一半是对象,一半是数据结构,既有执行操作的函数,又有getter/setter。同时增加了添加函数和添加数据结构的难度。
隐藏结构
经查,发现上述代码获取outputDir是为了根据路径得到BufferedOutputStream,创建文件,
String outFile = outputDir + "/" +className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout);
ctxt 应该仅仅暴露获取BufferedOutputStream的接口方法,隐藏具体实现。
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
-
数据传送对象
DTO 是只有公共变量(包括私有变量+公共getter/setter)、没有函数的类,是最精炼的数据结构。
Active Record 是一种特殊的DTO形式,同时也会拥有save、find方法,通常是对数据库或其他数据源的之间翻译(就是我们项目中的Domain Object,一个类对应数据库一张表)。Active Record往往被塞进业务规则方法,导致数据结构和对象的混杂体。
应该把Active Record当成数据结构,另外创建包含业务规则、隐藏内部数据的独立对象。
小结
-
数据结构暴露数据,没有明显行为。
过程式代码操作数据结构,添加新的函数无需修改数据结构,但添加新的数据结构需要修改所有函数。
-
对象暴露行为,隐藏数据。
面向对象式代码操作对象,添加新的类无需修改既有函数,但添加新的函数需要修改所有类。
不应对任何一种抱有成见,根据具体情况使用。
(ref: https://www.jianshu.com/p/822549096a82 )
第七章 错误处理
抽离Try/Catch代码块
函数应该只做一件事,错误处理就是一件事。
// bad public void delete(Page page) { try{ deletePage(page); registery.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey(); }catch(Expection e){ logError(e); } } // good public void delete(Page page) { try{ // 将上例的操作,封装到一个方法 deletePageAndAllReferences(page); }catch(Expection e){ logError(e); } }
使用非受控异常
受控异常: Checked Exception(FileException、SQLException等)
,这类异常必须写 try/catch
,或者 throw抛出
,否则编译通不过。
非受控异常: Unchecked Exception
,这类异常也叫做运行时异常(与非受控异常 字数相等),这类异常不需要 try/catch
,也不需要 throw抛出
。即使 throw 了,上层调用函数也非必须捕获,编译能通过。
受控异常的代价就是违反开放/闭合原则。如果你在方法中抛出受控异常,这意味着每个调用该函数的函数都要修改,捕获新异常,或在其签名中添加合适的throw语句。对于一般的应用开发,受控异常依赖成本要高于收益成本,尽量 try/catch
处理,不要抛出。
给出异常发生的环境说明
应创建信息充分的错误信息,并和异常一起传递出去。在消息中,包括 失败的操作和失败类型 。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。
依调用者需要定义异常类
// bad ACMEPort port = new ACMEPort(12); try { port.open(); } catch(DeviceResponseException e) { reportPortError(e); logger.log("Device response exception",e); } catch(ATM1212UnlockedException e) { reportPortError(e); logger.log("Unlock exception",e); } catch(GMXError e) { reportPortError(e); logger.log("Device response exception",e); } finally { // ..... }
通过打包调用API,确保它返回通过用异常类型,从而简化代码
// good LocalPort port = new LocalPort(12); try { port.open(); } catch(PortDeviceFailure e) { reportError(e); logger.log(e.getMessage(),e); } finally { // ..... } public class LocalPort{ private ACMEPort innerPort; public LocalPort(int portNumber){ innerPort = new ACMEPort(portNumber); } public open() { try { innerPort.open(); } catch(DeviceResponseException e) { // 自定义的异常类 throw new PortDeviceFailure(e); } catch(ATM1212UnlockedException e) { throw new PortDeviceFailure(e); } catch(GMXError e) { throw new PortDeviceFailure(e); } } }
将第三方API打包是个良好的实践手段。当你打包一个第三方API,你就降低了对它的依赖。
(ref: https://segmentfault.com/a/1190000015995913 )
采取特例模式(SPECIAL CASE PATTERN),创建一个类或配置一个对象,用来处理特例。客户代码就不用应付异常行为了。
不要返回或传递null值
方法返回null,不如抛出异常或返回特例对象,否则会有NullPointerException的隐患。
小结
将错误处理和主要逻辑隔离,就能写出整洁而强壮的代码。
以上所述就是小编给大家介绍的《《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 书中自有黄金屋(五):读《正面管教》
- 号外:友户通支持企业自有用户中心啦
- 书中自有黄金屋(二):读《穷爸爸富爸爸》
- 360浏览器推出自有根证书计划 吁加快证书安全技术改造
- 已获4轮融资,数据操作系统「Mesosphere」想要推动自有底层技术架构
- 英特尔放弃Lustre——芯片巨头宣布停止提供自有品牌高性能计算文件系统
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
区块链核心算法解析
【瑞士】Roger Wattenhofer(罗格.瓦唐霍费尔) / 陈晋川、薛云志、林强、祝庆 / 电子工业出版社 / 2017-8 / 59.00
《区块链核心算法解析》介绍了构建容错的分布式系统所需的基础技术,以及一系列允许容错的协议和算法,并且讨论一些实现了这些技术的实际系统。 《区块链核心算法解析》中的主要概念将独立成章。每一章都以一个小故事开始,从而引出该章节的内容。算法、协议和定义都将以形式化的方式描述,以便于读者理解如何实现。部分结论会在定理中予以证明,这样读者就可以明白为什么这些概念或算法是正确的,并且理解它们可以确保实现......一起来看看 《区块链核心算法解析》 这本书的介绍吧!