代码整洁之道8-14【读书笔记】

第八章(边界)

我们很少控制系统中的全部软件。有时我们购买第三方程序包或使用开放源代码,有时我能依靠公司中其他团队打造组件或子系统。不管是哪种情况,我们都得将外来代码干净利落地整合进自己的代码中。

使用第三方代码

接口提供者追求普适性,这样就能在多个环境中工作,吸引使用者。这种张力会导致系统边界上出现问题。举例比如java.util.Map。我们建议不要将边界上的其它接口在系统中传递。如果你使用类似Map这样的边界接口,就把它保留在类或近亲类中。避免从API中返回边界接口,或将边界接口作为参数传递给公共API。

浏览和使用边界

为要使用的第三方代码编写测试,通过核对试验来检测自己对那个API的理解程度。测试聚焦于我们想从API得到的东西。

学习log4j

学习性测试的好处不只是免费

学习性测试毫无成本。无论如何我们都得学习要使用的API,而编写测试则是获得这些知识的容易而不会影响其他工作的途径。学习性测试是一种精确试验,帮助我们增进对 API的理解。学习性测试不光免费,还在投资上有正面的回报。当第三方程序包发布了新版本,我们可以运行学习性测试,看看程序包的行为有没有改变。学习性测试确保第三方程序包按照我们想要的方式工作。一旦整合进来,就不能保证第三方代码总与我们的需要兼容。原作者不得不修改代码来满足他们自己的新需要。他们会修正缺陷、添加新功能。风险伴随新版本而来。如果第三方程序包的修改与测试不兼容,我们也能马上发现。

使用尚不存在的代码

还有另一种边界,那种将已知和未知分隔开的边界。在代码中总有许多地方是我们的知识未及之处。有时,边界那边就是未知的(至少目前未知)。有时,我们并不往边界那边看过去。

整洁的边界

边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中的信息。依靠你能控制的东西,好过依靠控制不了的东西,免得日后受它控制。

第九章(单元测试)

TDD三定律

编写生产代码前编写单元测试;
定律一:在编写不能通过的单元测试前,不可编写生产代码。
定律二:只可编写刚好无法通过的单元测试,不能编译也算不过。
定律三:只可编写刚好足以通过当前失败测试的生产代码。

保持测试整洁

脏测试等同于没测试。问题在于,测试必须随生产代码的演进而修改。测试越脏,就越难修改。测试代码越缠结,你就越有可能花更多时间塞进新测试,而不是编写新生产代码。修改生产代码后,旧测试就会开始失败,而测试代码中乱七八糟的东西将阻碍代码再次通过。于是,测试变得就像是不断翻番的债务。
测试代码和生产代码一样重要。它可不是二等公民。它需要被思考、被设计和被照料。它改像生产代码一般保持整洁

整洁的测试

三要素:可读性、可读性和可读性。在单元测试中,可读性甚至比在生产中还重要。明确、简洁和足够的表达力。以尽可能少的文字表达大量内容。第一环节构造测试数据,第二环节操作测试数据,第三部分检验操作是否得到期望的结果。

面向特定领域的测试语言

帮助程序员编写自己的测试,也可以帮助后来者阅读测试。

双重标准

有些事你大概永远不会在生产环境中做,而在测试环境中却完全没问题。通常这关乎内存或CPU效率的问题,不过却永远不会与整洁有关。

每个测试一个断言

每个测试函数中只测试一个概念。不要超长的测试函数,测试完这个又测试那个。

F.I.R.S.T.

整洁的测试还遵循5条规则。

  • 快速(fast) 测试应该能快速运行,太慢了你就不会频繁的运行,就不会尽早发现问题。
  • 独立(independent) 测试应该相互独立,某个测试不应该为下个测试设定条件。当测试相互依赖,一个没通过导致一连串的测试失败,使问题诊断变的困难。
  • 可重复(repeatable) 测试应该可以在任何环境中重复通过。
  • 自足验证(self-validating) 测试应该有布尔值输出,无论通过或失败,不应该是查看日志文件去确认
  • 及时(timely) 单元测试应该恰好在使其通过的生产代码之前编写。

小结

如果你坐视测试腐坏,那么代码也会跟着腐坏。保持测试整洁吧

第10章(类)

类的组织

标准的java约定,类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。公共函数应跟在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧随在公共函数后面。这符合了自顶向下原则,让程序读起来就像一篇报纸文章。

类应该短小

短小再短小。一个类不要承担太多权责。

单一权责原则

类或模块应有且只有一条加以修改的理由。系统应该有许多短小的类而不是巨大的类组成,每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

内聚

如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。内聚性高,意味着类中的方法和变量相互依赖,相互结合成一个逻辑整体。

持内聚性就会得到许多短小的类

一旦发现类失去内聚性(堆积了越来越多只为允许少量函数共享而存在的实体变量),就拆分它!

为了修改而组织

多数系统,修改将一直持续。每处修改都让我们冒着系统其他部分不能如期望般工作的风险。在整洁的系统中,我们对类加以组织,以降低修改的风险。调用方依赖抽象而不依赖于具体细节,符合依赖倒置原则。隔离具体细节,也不利于单元测试。

第11章(系统)

如何建造一个城市

每个城市都有一组组人管理不同的部分,供水系统,供电系统、交通、立法、执法等。有些人负责全局,其他人负责细节。

将系统的构造与使用分开

构造与使用是非常不一样的过程。软件系统应将启始过程和之后的运行时逻辑分开,在启始过程中构建应用对象,也会存在互相纠缠的依赖关系。

分解main

将全部构造过程迁移到main的模块中。

工厂

有时应用程序也要负责确定何时创建对象,这种情况下我们使用抽象工厂模式。

依赖注入

实现分离构造与使用,那就是依赖注入,控制反转在依赖管理中的一种应用手段。在依赖管理情景中,对象不应该负责实例化对自身的依赖,反之,它应该将这份权责移交给其他有权利的机制,从而实现控制的反转。

扩容

软件系统与物理系统可以类比。它们的架构都可以递增式地增长,只要我们持续将关注面恰当地切分。

Java代理

java代理使用于简单的情况,例如在单独的对象或类中包装方法调用。然而,JDK提供的动态代理仅能与接口协同工作。对于代理类,你得使用字节码操作库,比如CGLIB、ASM或者javassist.

纯Java AOP框架

AspectJ的方面

测试驱动系统架构

优化决策

模块化和关注面切分成就了分散化管理和决策。

明智使用添加了可论证价值的标准

系统需要领域特定语言

第12章(迭进)

通过迭进设计达到整洁目的

遵循以下规则,设计就能变得“简单”:

  • 运行所有测试;
  • 不可重复;
  • 表达了程序员的意图;
  • 尽可能减少类的方法的数量;
    以上规则按其重要程度排列。

简单设计规则:运行所有测试

不可测试的系统,绝不应部署。只要系统可测试,就会导向保持类短小且目的单一的设计方案。紧耦合的代码难以编写测试。同样编写测试越多,就会越遵循DIP之类的原则,使用依赖注入,接口和抽象等工具尽可能减少耦合。如此一来设计就会有长足进步。遵循有关编写测试并持续运行测试的、明确的规则,系统就会更贴近OO低耦合度、高内聚的目标。

简单设计规则2~4:重构

有了测试,就能保持代码和类的整洁,方法就是递增式地重构。测试消除了对清理代码就会破坏代码的恐惧。在重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度,切分关注面,模块化系统关注面,缩小函数和类的尺寸,选用更好的名称等。这也是应用简单设计三条规则的地方:消除重复,保证表达力,尽可能的减少类和方法的数量。

不可重复

重复代表着额外的工作、额外的风险和额外不必要的复杂度。极其雷同的代码当然是重复。类似的代码往往可以调整地更相似。

表达力

软件项目的主要成本在于长期维护。作者把代码写得越清晰,其他人花在理解代码上的时间也就越少,从而减少缺陷,缩减维护成本。所以花点时间在每个函数和类上。选用较好的名称,将大函数切分为小函数,等。。。

尽可能少的类和方法

我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍。这在四条规则里优先级最低,类和函数尽量少是重要但是测试,消除重复和表达力更重要。不要教条主义。

第13章(并发编程)

对象是过程的抽象。线程是调度的抽象。

为什么要并发

并发是一直解耦策略。它帮我们把做什么(目的)和何时(时机)做分解开。
在单线程应用中,目的与时机紧密耦合。而解耦目的与时机能明显地改进应用程序的吞吐量和结构。从结构的角度看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。
常见的迷思和误解:

  • 并发总能改进性能:并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。
  • 并发编程无需修改设计:并发算法的设计可能与单线程系统的设计极不相同,目的与时机的解耦往往对系统结构产生巨大影响。
  • 在采用Web和EJB容器时,理解并发问题不重要。

实际上最好了解容器在做什么,如何应付并发更新、死锁等问题。下面是一些编写并发软件的中肯说法:

  • 并发会在性能和编写额外代码上增加一些开销。
  • 正确的并发是复杂的,即使对于简单的问题也是如此。
  • 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,而未被当做真的缺陷看待。
  • 并发常常需要对设计策略的根本性修改。

挑战

举了个例子,多数路径都能得到正确结果。问题是其中一些不能得到正确结果。

并发防御原则

单一权责原则

并发设计需要考虑的一些问题:

  • 并发相关代码有自己的开发、修改和调优生命周期
  • 开发相关代码有自己要对付的挑战,和非并发代码不同,往往更困难。
  • 即便没有周边应用程序增加的负担,写的不好的并发代码可能的出错方式数量也已经足具挑战性。

建议:分离并发相关代码与其他代码。

推论:限制数据作用域

两个线程修改共享对象的同一字段时,可能互相感人,导致未预期的行为。解决方案之一是采用synchronized关键字保护一块使用共享对象的临界区。更新共享数据的地方越多,就约可能:
忘记保护一个或多个临界区—破坏了修改共享数据的代码;
你得多花力气保证一切都受到有效保护;
很难找到和判断错误源。

建议: 谨记数据封装;严格限制对可能被共享的数据的访问。

推论:使用数据复本

在某些情形下可能复制对象以只读方式对待,或者从多个线程收集所有复本的结果,并在单个线程中合并这些结果。

推论:线程应尽可能地独立

让每个线程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所以请求数据,存储为本地变量。这样每个线程都是唯一的,没有通不需要。

建议:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。

了解Java库

  • 使用Java类库提供的线程安全群集;
  • 使用executor框架执行无关任务;
  • 尽可能使用非锁定解决方案;
  • 有几个类并不是线程安全的。

了解执行模型

先了解下基础定义:
限定资源、互斥、线程饥饿、死锁、活锁。

生产者-消费者模型

一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一直限定资源。

读者-作者模型

当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就是个问题。增加吞吐量会导致线程饥饿和过时信息的累积。更新会影响吞吐量。挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。

宴席哲学家

进程竞争资源,设计不合理就会遭遇死锁、活锁、吞吐量和效率降低等问题。

警惕同步方法之间的依赖

JAVA语言的synchronize可以保护单个方法,然而如果在同一个共享类中有多个同步方法,就可能不太正确了。

建议: 避免使用一个共享对象的多个方法。
必须要使用一个共享对象的多个方法的时候,有3种写代码的手段:

  • 基于客户端的锁定—客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;
  • 基于服务端的锁定—在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法;
  • 适配服务端—创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码。

保持同步区域微小

关键字synchronize制造了锁。同一个锁维护的所有代码区域在任一时刻保证只有一个线程执行。锁是昂贵的,带来了延迟和额外开销。另外,临界区应该被保护起来。所以,尽可能少地设计临界区。将同步延展到最小临界区范围之外,会增加资源争用,降低执行效率。

建议: 尽可能减少同步区域。

很难编写正确的关闭代码

平静关闭很难做到。长建问题与死锁有关,线程一直等待永不会到来的新号。如果要编写涉及平静关闭的并发代码,请多预留一些时间搞关闭过程。

建议:尽早考虑关闭问题,尽早令其工作正常。检查既有算法,因为这可能会比想象中难,花费比预期更多的时间。

测试线程代码

多线程的测试涉及同一段代码和共享数据,会比较复杂。
建议:编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过就忽略失败。

精炼的建议:

  • 将伪失败看作可能的线程问题
  • 先使非线程代码可工作
  • 编写可插拔的线程代码
  • 编写可调整的线程代码
  • 运行多于处理器数量的线程
  • 在不同平台上运行
  • 调整代码并强迫错误发生
将伪失败看作可能的线程问题

线程代码中的缺陷可能在一千或一百万次执行中才会显现一次。不要将系统错误归咎于偶发事件。

先使非线程代码可工作

不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。

编写可插拔的线程代码

编写可在数个配置环境下运行的线程代码:

  • 单个线程与多个线程在执行时不同的情况;
  • 线程代码与实物或测试替身互动;
  • 用运行快速、缓慢和有变动的测试替身执行;
  • 将测试配置为能运行一定数量的迭代。
    建议:编写可插拔的线程代码,就能在不同的配置环境下运行。
编写可调整的线程代码

要获得良好的线程平衡,需要试错。一开始,在不同的配置环境下检测系统性能。要允许线程数量可调整。在系统运行时允许线程发生变动。允许线程依据吞吐量和系统使用率自我调整。

运行多于处理器数量的线程

系统在切换任务时会发生一些事。为了促使任务交换的发生,运行多于核心处理器数量的线程。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。

在不同平台上运行

尽早并经常地在所有目标平台上运行线程代码。

装置试错代码

并发代码中的缺陷,不频繁,难以发现,只有少数路径会真的导致失败。
可以装置代码,增加对Object.wait()、Object.sleep()、Object.yield()和Object.priority()等方法的调用,改变代码执行顺序。从而增加了侦测到缺陷的可能。有两种装置代码的方法:

  • 硬编码
  • 自动化
硬编码

手动向代码中插入wait()、sleep()、yield()和priority()的调用。改变代码的执行路径。这种手法有毛病:

  • 你得手动找到合适的地方来插入方法调用;
  • 怎么知道在哪里插入调用,插入什么调用;
  • 不必要地在产品环境中留下这类代码,将拖慢代码执行速度;
  • 这种无的放矢的手动,可能找不到缺陷。

如果将系统分解为对线程及控制线程的类一无所知的POJO,就能更容易地找到装置代码的位置,还能创建许多个以不同方式调用sleep、yield等方法的POJO测试。

自动化

可以使用Aspect-Oriented Framework、CGLIB或ASM之类工具通过编程来装置代码。
有一种叫做ConTest的工具,由IBM开发,能做类似的事情。
要点是让代码“异动”,从而使线程以不同次序执行。编写良好的测试与“异动”组合,能有效地增加发现错误的机会。
建议:使用异动策略搜出错误。

第14章(逐步改进)

一个命令行参数解析程序的案例改进。

Args案例实现

要写整洁代码,必须先写肮脏代码,然后再清理它。“能工作”的程序不能是最终程序。

Args:草稿

能工作,却很烂。

所以我暂停了

至少还有两种参数类型要添加,一味蛮干会留下一堆要调整的混乱,所以重构。

渐进

毁坏程序最好的方法之一就是以改进之名大动其结构。采用测试驱动开发的规程。原则之一是保持系统始终能运行。采用TDD,每次修改都必须保证系统能像以前一样工作。

字符串参数

案例参数扩展

小结

代码能工作是不够的。能工作的代码经常会严重崩溃。随着代码腐败下去,模块之间互相渗透,出现大量隐藏纠结的依赖关系。解决之道就是保持代码持续整洁和简单。永不让腐坏有机会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值