编者寄语:这是一本真正的好书,不过如果读者没有一定的经验,以及缺乏对编程境界的追求的话,可能认为这本书很一般。当然,对于有心人来说,这本书里面的部分东西可能都已经习以为常了。
那么,你是怎样的呢?
另外我为什么写的是《clean code》而不是《代码整洁之道》,因为这本书很多地方你需要看原版的文字才能get到作者真正想表达的意思。如果有能力还是看原版吧。
看原版书,你能学到很多术语表达,在你看外文技术文章的时候更容易帮助你理解全文。如increase cohesion - 增加内聚性,decrease coupling - 减少耦合,separate concerns - 关注点分离,modularize system concerns - 模块化系统关注点,这些都是很经典的表达。
I sincerely and strongly recommend u to read 《clean code》 rather than 《代码整洁之道》
原文地址:《clean code》 阅读笔记转载请注明出处!
关键词:优雅
整洁的代码读起来令人愉悦
比如"customerObject"和"customer", "ProductInfo"和"ProductData";这种就是意义混杂的废话。如果真的有区别,就用特定的可以区分的命名来描述它。
找MAX_CLASSES_PER_STUDENT
很容易,但想找数字7就麻烦了。
如postPayment、deletePage或save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。
我的理解中,在同个领域模型中,就应该只有一个命名,比如订单号,同个系统中不应该出现TradeNo、OrderNo等多个命名。
尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语。
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称。
编程就像讲故事,要用准确、清晰、富有表达力的语句(代码)
重要的事情说3遍。
每个函数一个抽象层级!!!
这个是编者认为非常重要的一点,也是本人在开发过程当中看到最多的问题。应该处于不同抽象层级的代码混乱在一起时,阅读和理解起来会很痛苦。
引原文描述:函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
但是,就像作者说的,这条规则很难。
长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。
为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。
例: CopyUtil.copyToDB(isWorkDB) --> CopyUtil.copyToWorkDB(), CopyUtil.copyToLiveDB()
(但是编者阅读很多源码里面也没有遵守,手动狗头...)
这节实际上内容不多,尽量避免注释
好的注释:
垂直距离
水平位置
这个在常见的IDE中可以设置提示线。下图是IDEA的配置位置。
效果:
不要使用不同的风格来编写源代码,会增加其复杂度。
这块有两个我比较在意的概念
The Law of Demeter:模块不应了解它所操作对象的内部情形。
更准确更白话地说:方法不应调用由任何函数返回的对象的方法。只跟朋友谈话,不与陌生人谈话。
反例:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
使用不可控异常(这点深有体会,checked Exception的代价很大)
这里作者想说明的是,在使用受检异常时,你首先要考虑这样是否能值回票价。因为受检异常违反了开闭原则,当你在一个方法内抛出了受检异常时,你就得在catch语句和抛出异常之间的方法调用链中的每个方法签名中声明这个异常。
这意味着,你对软件较低层级的修改,会涉及到较高层级的签名。封装被打破了,因为在抛出路径中的每个函数都要去了解下一层级的异常细节。既然异常旨在让你能在较远处处理错误,可控异常以这种方式破坏封装简直就是一种耻辱。
如果你在编写一套关键代码库,则可控异常有时也会有用:你必须捕获异常。但对于一般的应用开发,其依赖成本要高于收益。
给出异常发生的环境说明(这个也很重要)
创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。
良好的日志和异常机制,是不应该出现调试的。打日志和抛异常,一定要把上下文给出来,否则,等于在毁灭命案现场,把后边处理问题的人,往歪路上带。需要调试来查找错误时,往往是一种对异常处理机制的侮辱
尝试使用特例模式(SPECIAL CASE PATTERN),将异常行为封装到特例对象中。
很巧妙高级的一种设计模式。
// 修改前 try { MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); } catch(MealExpensesNotFound e) { m_total += getMealPerDiem(); } // 优化之后,当没有餐食消耗(即上述代码抛出MealExpensesNotFound的情况),返回特例对象 MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); // 特例对象 public class PerDiemMealExpenses implements MealExpenses { public int getTotal() { // return the per diem default } }
不要返回null,不要传递null
相信不少程序员都深受null 的困扰。返回null值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查null值,应用程序就会失控。
在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值。事已如此,恰当的做法就是禁止传入null值。
边界这一章个人读起来比较难懂。感觉像是翻译的问题。
原书这一章节的名字叫做"Boundaries"。
这一章篇幅较短,意义有点难懂,这里简单总结:作者的意思是让我们自己的代码和第三方库的代码不要耦合太紧密,需要有清晰的Boundaries。
同时也给出了第三方类库的学习建议:探索性地学习测试,以此熟悉类库,写出良好的代码。
它需要被思考、被设计和被照料。它该像生产代码一般保持整洁。
测试代码需要随着生产代码的演进而修改,如果测试不能保持整洁,只会越来越难修改。
单测本身也应该成为Code Review的一部分,单测写的好,bug一定少。
任何一种迭代和增量的交付方式,都会遇到一个严肃的灵魂拷问:频繁对软件做修改,如何保障软件不被改坏?这个问题,用人肉测试解决不了。交付越频繁,人肉测试就越不可能跟上节奏。自动化的、快速且可靠的、覆盖完善的测试必不可少。这种要求,后补式的、黑盒的测试方法不可能达到,必须在开发软件的过程中内建。当团队被迫采用迭代和增量的需求管理和项目管理方式,对应的配置管理和质量保障手段就必须跟上。TDD不是锦上添花,而是迭代和增量交付不可或缺的基石。
整洁的测试应该遵循以下5条规则:
测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。
测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。
测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。
测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。
测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。
对于衡量类的大小,这里书中提出了一个不同的衡量方法:计算权责。我理解的意思就是,一个类承担了太多的权责之后,这个类就算大了。
所以书中随即提出了SRP - 单一权责原则(也叫单一职责原则)。
单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。类只应有一个权责——只有一条修改的理由。
作者还提到了,系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
同时,作者提出了保持内聚性就会得到许多短小的类。
类的高内聚的含义是:类的实体变量应尽可能少,类中方法尽可能多地使用到这些变量。(如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性)
在整洁的系统中,我们对类加以组织,以降低修改的风险。
类应当对扩展开放,对修改封闭。通过子类化手段,类对添加新功能是开放的,而且可以同时不触及其他类。
DIP认为类应当依赖于抽象而不是依赖于具体细节。通过这种抽象隔离了系统之间的元素,使得系统每个元素的理解变得更加容易,使用起来更加灵活、更加可复用。
系统构造与使用分开。
这里我理解就是将一些对象实例的初始化和使用分离解耦,将构建实例的逻辑交给一个公共的模块/类/框架来做。这里作者也介绍了开发中常见的两种方式,体现了这种思想:
后半章主要讲的是AOP的思想和具体的框架实现。就是说将一些重复性、功能性的代码(如:性能监视、日志记录、事务管理、安全检查、缓存等)进行关注面切分,模块化,成就了分散化管理和决策。最终的效果也显而易见,减少了重复代码,关注面的分离也使得设计、决策更加清晰容易。
这一节主要是讲了四个简单的设计规则(design rules),通过遵循这四个规则,你可以编写出很好的代码,深入了解代码的结构和设计,继而以一种更简单的方式来学习掌握SRP和DIP之类的设计原则。
Four rules of Simple Design are of significant help increating well-designed software
全面测试并持续通过所有测试。遵循SRP的类,测试起来较为简单。测试编写得越多,就越能持续走向编写较易测试的代码。所以,确保系统完全可测试能帮助我们创建更好的设计。
有了全面的测试保驾护航之后,我们就有条件一步一步地去重构完善我们的代码,目的是为了得到“高内聚,低耦合”的系统。书中也提出了下面三条简单的规则。
当你在重构时,按照SRP、代码可读性等规则遵守,是有可能创建出比原来更多的细小的类。但这不在本条的针对范围之内。
这里的尽量减少,作者举例了一种情况,就是毫无意义的教条主义会导致编码人员无意识的创建很多的类和方法。不知道你有没有类似的经历,我拿我亲身体会举个例子,我很难理解在某个项目中,对一个领域对象(如User),在构建对应的Service层和Dao层的时候,一定要为每个类创建接口,即使这些接口根本不可能有其他的实现类。
“Objects are abstractions of processing. Threads are abstractions of schedule.”
—James O. Coplien
这一节作者讨论了并发编程的需求和难点,并且给出了一些解决这些困难和编写整洁并发代码的建议。因为关于并发编程有更好的资料可以学习,所以这里我就简单总结一下。
其他未提到的章节,是我觉得相较来说非重点的章节。还有可能会有一些内容的遗漏,因为这本书中的精华,我觉得我还需要学习领会。
好书常读常新,这本书就在我的工位上,我希望在经历一段时间的工作实践之后,再次打开这本书,我能有更多更新的一些感悟。
如果本文有帮助到你,希望能点个赞,这是对我的最大动力🤝🤝🤗🤗。