军规:让营地比你来时更干净。
Leblanc : Later equals never. (勒布朗法则:稍后等于永不)
对代码的每次修改都影响到其他两三处代码。修改无小事。如同医生不能遵从病人的意愿,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。赶上期限的唯一方法,做的快的唯一方法,就是始终尽可能保持代码整洁。破窗理论:环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。==一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上==。这个现象,就是犯罪心理学中的破窗效应。结论:及时矫正和补救正在发生的问题。启发:影响的大小并不能改变行为错误的本质,别人的错误更不会是证明你无错的理由。整洁的代码只做好一件事:整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。在意代码。简单代码:能通过所有测试;没有重复代码;体现系统中的全部设计理念;包括尽量少的实体,比如类,方法,函数等。不要重复代码,只做一件事,表达力,小规模抽象。不读周边代码的话就没法写代码。编写代码的难度,取决于读周边代码的难度。军规:让营地比你来时更干净。使用不可控异常:可控异常的代价就是违反开放/闭合原则
如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常之间的每个方法签名中声明该异常
给出异常发生的环境说明:你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。在java中,你可以从任何异常里得到堆栈踪迹,然而堆栈踪迹却无法告诉你该失败操作的初衷。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。依调用者需要定义异常类:定义常规流程:业务逻辑和错误处理代码之间就会有良好的间隔;你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中。
特例模式(Special Case Pattern):创建一个类或配置一个对象,用来处理特例。
别返回null值:别传递null值:小结:将错误处理隔离对待,独立于主要逻辑之外,就能写出强固而整洁的代码
整洁的测试:构造->操作->检验 (Build->Operate->check):
构造测试数据;操作测试数据;检验操作是否得到期望的结果
守规矩的开发者将它们的测试代码重构为更简洁和具有表达力的形式。双重标准:测试代码应当简单,精悍,足具表达力。双重标准指的是在关乎内存或CPU效率的问题。每个测试一个断言:每个测试函数都应该有且只有一个断言语句,或者说,单个测试中的断言数量应该最小化,每个测试函数只测试一个概念。F.I.R.S.T. : 快速(Fast), 独立(Independent), 可重复(Repeatable), 自足验证(Self-Validating), 及时(Timely)。
测试应该够快:测试运行缓慢你就不会想要频繁的运行它;测试应该相互独立;测试应当可在任何环境中重复通过;测试应该有布尔值输出,你不应该查看日志来确认是否通过,不应该手工对比两个不同文本来确认测试是否通过;测试应及时编写:单元测试应该恰好在使其通过的生产代码之前编写。
小结:如果你坐视测试腐败,那么代码也会跟着腐坏。
隔离修改:需求会改变,所以代码也会改变。具体类包含实现细节,而抽象类则只呈现概念。可以借助接口和抽象类来隔离这些细节带来的影响。
部件之间的解耦代表着系统中的元素相互隔离得很好。
降低连接度,我们的类就遵循了另一个类设计原则:依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP):类应当依赖于抽象而不是依赖于具体细节。程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
扩容:“一开始就做对系统”纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统,实现新的用户故事。这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及他们打造出的整洁代码,在代码层面保证了这个过程的实现。
软件系统与物理系统可以类比。它们的架构都可以递增式地增长,只要我们持续将关注面恰当地切分。
横贯式关注面:原则上,你可以从模块、封装的角度推理持久化策略。但在实践上,你却不得不将实现了持久化策略的代码铺展到许多对象中。 Java中的三个“方面”: - Java代理:适用于简单的情况,例如在单独的对象或类中包装方法调用。纯JAVA AOP框架:Spring AOP, JBoss AOPAspectJ的方面:AspectJ提供了一套用以切分关注面的丰富而强有力的工具。测试驱动系统架构:通过方面式手段切分关注面的威力不可低估,假使你能用POJO编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构。采用一些新技术,就能将架构按需从简单演化到精细。没必要先做大设计(Big Design Up Front, BDUF)。实际上,BDUF甚至是有害的,它阻碍改进,因为心理上会抵制丢弃既成之事,也因为架构上的方案选择影响到后续的设计思路。
最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。
优化决策:模块化和关注面切分成就了分散化管理和决策。最好是授权给最有资格的人,延迟决策至最后一刻也是好手段,让我们能够基于最有可能的信息作出选择。提前决策是一种预备知识不足的决策。如果决策太早,就会缺少太多客户反馈、关于项目的思考和实施经验。
拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂性也降低了。
明确使用添加了可论证价值的标准:有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合。系统需要领域特定语言:领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节使用POJO来表达。小结:系统应该是整洁的。侵害性架构会湮灭领域逻辑,冲击敏捷能力。在所有的抽象层级上,意图都应该更加清晰可辨。只有在编写POJO并使用类方面的机制来无损地组合其他关注面时,这种事情才会发生。
无论是设计系统或单独的模块,别忘了使用大概可工作的最简单方案。
对象是过程的抽象。线程是调度的抽象。
为什么要并发:并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。解耦目的与时机能明显地改进应用程序的吞吐量和结构。应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。例如Servlet,当有web请求时,servlet就会异步执行,每次Servlet是在自己的小世界中执行,与其他servlet的执行是分离的。 并发会在性能和编写额外代码上增加一些开销;正确的并发是复杂的,即便对于简单的问题也是如此;并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待;并发常常需要对设计策略的根本性修改。挑战:需要理解Just-In-Time编译器如何对待生成的字节码,理解Java内存模型认为什么东西具有原子性。并发防御原则: 单一权责原则(SRP):方法、类、组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由,所以也该从其他代码中分离出来。建议:分离并发相关代码与其他代码 并发相关代码有自己的开发、修改和调优生命周期;并发相关代码有自己要对付的挑战,和非并发相关代码不同,而且往往更为困难;即便没有周边应用程序增加的负担,写的不好的并发代码可能的出错方式数量也已经足具挑战性。推论:限制数据作用域——采用synchronized关键字在代码中保护一块使用共享对象的临界区。限制临界区的数量很重要。建议:谨记数据封装,严格限制对可能被共享的数据的访问。推论:使用数据复本:避免共享数据的好方法之一就是一开始就避免共享数据。如果有避免共享数据的简易手段,结果代码就会大大减少导致错误的可能。推论:线程应尽可能地独立:让每个线程在自己的世界中存在,不与其他线程共享数据。建议:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。了解Java库: 使用类库提供的线程安全群集;使用executor框架执行无关任务;尽可能使用非锁定解决方案;有几个类并不是线程安全的。 线程安全群集: java.util.concurrent.*java.utl.concurrent.atomic.*;java.util.concurrent.locks.*;ReentrantLock,可在一个方法中获取,在另一个方法中释放的锁;Semaphore 经典的“信号”的一种实现,有计数器的锁CountDownLatch,在释放所有等待的线程之前,等待制定数量事件发生的锁。这样,所有的线程都平等地几乎同时启动。 了解执行模型: -定义限定资源有着固定尺寸或数量的资源,如数据库连接或固定尺寸的读/写缓存等互斥每个时刻仅有一个线程能访问共享数据数据或共享资源线程饥饿一个或一组线程在很长时间内或永久被禁止死锁两个或多个线程互相等待执行结束活锁执行次序一致的线程,每个都想起步,但发现其他线程已经“在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动 (一直重复尝试—失败—尝试—失败的过程) 生产者-消费者模型:生产者消费者之间的队列是一种限定资源;读者-作者模型:主要为读者提供信息源,偶尔被作者线程更新的共享资源,吞吐量是一个问题。宴席哲学家:企业级应用中进程竞争资源的情形。 建议:学习这些基础算法,理解其解决方案,则能在遇到并发问题时你就能有解决问题的准备了警惕同步方法之间的依赖: 建议:避免使用一个共享对象的多个方法有时必须使用一个共享对象的多个方法,有3种方法: 基于客户端的锁定:客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;基于服务端的锁定:在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法。适配服务端:创建执行锁定的中间层。这是一种基于服务端锁定的例子,但不修改原始服务端代码。保持同步区域微小:synchronized锁是昂贵的,会带来延迟和额外开销。应该尽可能少的设计临界区。 建议:尽可能减小同步区域很难编写正确的关闭代码: 编写永远运行的系统,与编写运行一段时间后平静地关闭的系统是两码事。建议:尽早考虑关闭问题,尽早令其正常工作。这会花费比你预期更多的时间。检视既有算法,因为这可能比想象中难的多。测试线程代码: 建议:编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败。精炼的建议: 将伪失败看作可能的线程问题:不要将系统错误归咎于偶发事件。先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。编写可插拔的线程代码: 单线程与多个线程在执行时不同的情况;线程代码与实物或测试替身互动;用运行快速、缓慢和有变动的测试替身执行;将测试配置为能运行一定数量的迭代编写可调整的线程代码:允许线程数量可调整,在系统运行时允许线程发生变动。允许线程依据吞吐量和系统使用率自我调整。运行多于处理器数量的线程:系统在切换任务时会发生一些事,为了促使任务交换的发生,运行多于处理器或处理器核心数量的线程。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。在不同平台上运行:不同的操作系统有着不同的线程策略。尽早并经常地在所有目标平台上运行线程代码。装置试错代码:两种装置代码的方法 硬编码:手工向代码中插入wait(),sleep(),yield(),priority()。自动化:(要点是让代码“异动”)使用Aspect-Oriented Framework,CGLIB或ASM之类工具通过编程来装置代码:一个有单个方法jiggle()的类,第一种方法jiggle()什么都不做,第二种实现生成一个随机数,在睡眠、让步或径直执行间做选择。上千次的做这种随机测试,大概就能找到一些缺陷的根源。调整代码并强迫错误发生:小结:要编写并发代码,就得严格地编写整洁的代码,否则将面临微细和不频繁发生的失败。第一要诀是遵循单一权责原则。将系统分离了线程相关代码和线程无关代码的POJO代码修改的原因
注释: 不恰当的信息:注释只应该描述有关代码和设计的技术性信息。废弃的注释:过时、无关或不正确的注释就是废弃的注释。冗余注释:注释应该谈及代码自身没提到的东西糟糕的注释:值得编写的注释,也值得好好写。注释掉的代码环境: 需要多步才能实现的构建:构建系统应该是单步的小操作。需要多步才能做到的测试:应当能够发出单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速、轻易和直截了当地做到。函数 过多的参数,没参最好输出参数:输出参数就违反直觉。如果函数非要修改什么东西的状态不可,就修改它所在对象的状态好了。标识参数:布尔值参数大声宣告函数做了不止一件事。死函数:永不被调用的方法应该丢弃。别害怕删除函数。一般性问题: 一个源文件中存在多种语言:理想的源文件包括且只包括一种语言,应该尽力减少源文件中额外语言的数量和范围。明显的行为未被实现:最小惊异原则--函数或类应该实现其他程序员有理由期待的行为。不正确的边界行为:别依赖直觉。追索每种边界条件,并编写测试。忽视安全:关闭失败测试、告诉自己过后再处理,这和假装刷信用卡不用还钱一样坏。重复:核心原则。每次看到重复代码,都代表遗漏了抽象。在错误的抽象层级上的代码:只与细节实现有关的常量、变量或工具函数不应该在基类中出现。基类依赖于派生类:例外情况是派生类数量严格固定。信息过多:设计良好的模块有着非常小的接口,让你能事半功倍。设计良好的接口并不提供许多需要依靠的函数,所以耦合度也较低。设计低劣的接口提供大量你必须调用的的函数,耦合度较高。隐藏你的数据。隐藏你的工具函数。隐藏你的常量和你的临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护变量和函数。尽量保持接口紧凑。通过限制信息来控制耦合度。死代码:死代码就是不执行的代码。垂直分隔:变量和函数应该在靠近被使用的地方定义。前后不一致:从一而终,小心选择约定,一旦选中,就小心持续遵循。混淆视听:人为耦合:不互相依赖的东西不该耦合。特性依恋:类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。“选择算子”参数:“选择算子”参数只是一种避免把大函数切分为多个小函数的偷懒做法。选择算子不一定是boolean类型,可能是枚举元素、整数或任何一种用于选择函数行为的参数。晦涩的意图:代码要尽可能具有表达力。位置错误的权责:软件开发者做出的最重要决定之一就是在哪里放代码。(最小惊异原则)代码应该放在读者自然而然期待它所在的地方。不恰当的静态方法:恰当的静态方法不应在单个实体上操作。应当使用解释性变量:让程序可读的最有力的方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。函数名称应该表达其行为:理解算法:“可以工作”是不行的,必须知道解决方案是正确的。应当把逻辑依赖改为物理依赖:依赖者模块不应对被依赖者模块有假定。应当用多态替代if/else或switch/case: 在使用if/else或switch/case前,先考虑使用多态。遵循标准约定:用命名常量替代魔术数:准确:结构甚于约定:坚守结构甚于约定的设计决策。命名约定很好,但却次于强制性的结构。封装条件:应该把解释了条件意图的函数抽离出来。避免否定性条件:函数只做一件事:不要掩蔽时序耦合:通过创建时序队列暴露时序耦合。应当封装边界条件:函数应当只在一个抽象层级上:(拆分不同抽象层级是重构的最重要的功能之一)应当在较高层级放置可配置数据避免传递浏览:确保模块只了解其直接协作者。Java 通过使用通配符避免过长的导入清单:(这一项由IDE来实现)不要继承常量:常量 VS. 枚举 :优先用枚举采用描述性名称:名称应与抽象层级相符尽可能使用标准命名法:无歧义的名称:避免编码:不应在名称中包括类型或作用范围信息。名称应该说明副作用:名称应该说明函数、变量或类的一切信息。测试: 测试不足:使用覆盖率工具:覆盖率工具能汇报你测试策略中的缺口。别略过小测试被忽略的测试就是对不确定事物的疑问:需求不明确而不能确定某个行为细节,可以用注释掉的测试或者用@Ignore标记的测试来表达我们对于需求的疑问。测试边界条件全面测试相近的缺陷测试失败的模式有启发性测试覆盖率的模式有启发性测试应该快速转载于:https://www.cnblogs.com/AbelZone/p/10049953.html
相关资源:《Clean Code-代码整洁之道》(中文完整高清扫描版)