第三章 函数
- 短小
- 只做一件事
- 每个函数一个抽象层级
- switch语句
- switch语句容易过长,特别是当逻辑都写在switch所在的类中时,容易:
- 违反单一权责原则(SRP)
- 违反开放闭合原则(OCP)
- 解决方案:
- 将switch语句埋到抽象工厂下
- 使用switch语句为排程逻辑创建新的实体(不是switch接受参数然后直接执行,而是返回
new
的子类对象) - 即只出现一次,用于创建多态对象,而且隐藏在继承关系中。
- switch语句容易过长,特别是当逻辑都写在switch所在的类中时,容易:
- 函数参数
- 尽量避免函数参数大于三个
- 标识参数
- 向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。
- 参数对象
- 如果函数看起来需要2、3个或3个以上的参数,就说明其中一些参数应该封装为类了。
- 分隔指令与询问(分离读和写。eg. 先通过读判断是否需要写,然后写函数,不要写到一个函数里,或明确函数名字)
- 使用异常代替返回错误码(错误处理代码可以从主路径代码中分离)
- 抽离Try/Catch代码块,另外形成函数
- 错误处理就是一件事。函数应该只做一件事,因此处理错误的函数不该做其他事,try应该是该函数第一个单词,catch/finally后也不该有其他内容。
第四章 注释
- 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。最好不是用注释而是用代码来表达。
第六章 对象和数据结构
得墨忒定律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形。
- 隐藏结构:把逻辑交给对象去做,而不是从对象中获取内部结构,然后自己做。
数据传输对象(DTO):只有公共变量、没有函数的类。
第七章 错误处理
使用不可控异常:
- 可控异常的代价是违反开放/闭合原则,如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常。
定义常规流程:
- 特例模式(Special Case Pattern)。创建一个类或配置一个对象用来处理特例。你来处理特例,客户代码就不用处理异常行为了。异常行为被封装到特例对象中。
eg. 特例是调用函数在内部遇到特殊情况时,返回一个继承/继承同一接口的另一种多态对象,特殊情况在该类对象中处理,而不是在外部处理。
别返回null值。可以抛出异常或返回特例对象。如果是返回集合类型,可以考虑返回空集合。
第九章 单元测试
整洁的测试:
- 呈现了 构造-操作-检验(Build-Operate-Check)
每个测试一个概念
- 每个测试函数只测试一个概念
F.I.R.S.T. 快速的测试应遵循
- 快速
- 独立
- 可重复:在任何环境中重复通过
- 自足验证:应该有布尔值输出而非查看日志来判断是否通过
- 及时(Timely):测试应该及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。
第十章 类
类应该短小
- 类的名称应当描述其权责。类名越含混,该类越有可能拥有过多的权责。例如,如果类名中包含
- 单一权责原则(SRP):类或模块应有且只有一条加以修改的理由。
- 内聚:类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。
- 一般来说,创建这种极大化内聚类是既不可取也不可能的;另一方面我们希望内聚性保持在较高的位置。内聚性高,意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体。
- 保持函数和参数列表短小的策略,有时会导致一组子集方法所使用的实体变量数量增加。出现这种情况时往往意味着至少有一个类要从大类中挣扎出来。你应当尝试将这些变量和方法拆分到两个或多个类中,让新的类更为内聚。
- 保持内聚性就会得到许多短小的类
- 将一个有许多变量的大函数拆为单独的小函数,是否要将许多变量作为参数传到新函数?
- 没必要,将这些变量提升为类的实体变量。但是类丧失了内聚性,只为允许少量函数共享而存在的实体变量越来越多。
- 这些函数想要共享变量,为什么不让他们有自己的类呢?——将大函数拆为小函数,往往也是将类拆分为小类的时机。
为了修改而组织
- 隔离修改:利用多态,将新的逻辑写进新的多态类中,调用该逻辑的函数参数类则是抽象类(或接口)。通过降低链接度,我们的类还遵循了另一条类设计原则,依赖倒置原则(Dependency Inversion Principle,DIP),DIP认为来应当依赖于抽象而不是依赖于具体细节。
第十一章 系统
如何在更高的抽象层级——系统层级——上保持整洁
将系统的构造与使用分开
- 构造与使用是非常不一样的过程。(类比一下建筑的建设和使用)
- 软件系统在启始的过程中构建应用对象,也会存在相互缠结的依赖关系。
- eg. 延迟初始化:测试是个问题、而且违反单一权责原则
- 分解main
- 将全部构造过程搬迁到main或被称之为main的模块中,设计系统其余部分时,假设所有对象都已经正确构造和设置
- 工厂
- 让应用自行控制何时创建,但不感知构造细节
- 依赖注入(Dependency Injection,DI),控制反转(Inversion of Control,IoC)在依赖管理中的一种手段.
- 控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则
扩容
- 横贯式关注面:面向切面编程(aspect-oriented programming,AOP)
Java代理
纯Java AOP框架
AspectJ的方面
测试驱动系统架构
- 最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来。这种架构能测试驱动,就像代码一样。
明智使用添加了可论证价值的标准
系统需要领域特定语言(Domain-Specific Language,DSL)
第十二章 迭进
通过迭进设计达到整洁目的:Kent Beck关于“简单设计”的四条规则
- 运行所有测试
- 不可重复
- 表达了程序员的意图
- 尽可能减少类和方法的数量
简单设计规则1:运行所有测试
- 只要系统可测试,就会导向保持类短小且目的单一的设计方案。遵循SRP的类,测试起来较为简单。测试编写得越多,就越能持续走向编写较易测试的代码。
- 紧耦合的代码难以编写测试。编写测试越多,就越会遵循DIP之类的规则,使用依赖注入、接口和抽象等工具尽可能减少耦合。
简单设计规则2~4:重构
- 有了测试,就能保持代码和类的整洁,方式就是递增式地重构代码。
- 添加了几行代码后,就要琢磨一下变化。设计退步了吗?如果是就要清理,并运行测试,保证没有破坏任何东西。测试消除了对清理代码就会破坏代码的恐惧。
不可重复
- “小规模复用”可大量降低系统复杂性
- 模板方法模式
表达力
- 好名称。类名、函数名
- 保持函数和类尺寸短小
- 采用标准命名法
- 编写良好的单元测试
- 不断尝试重构
尽可能少的类和方法
- 不要过渡使用消除重复、代码表达力和SRP等基础概念
- 本条优先级较低
第十三章 并发编程
“对象是过程的抽象,线程是调度的抽象。”——James O Coplien
为什么要并发
- 并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。
- 例如,Web应用的Servlet标准模式
- 并发常常需要对设计策略的根本性修改
并发防御原则
- 单一权责原则
- 分离并发相关代码与其他代码
- 推论:限制数据作用域
- 谨记数据封装;严格限制对可能被共享的数据的访问
- 推论:使用数据复本
- 推论:线程应尽可能地独立
- eg. HttpServlet
了解Java库
了解执行模型
- 生产者-消费者模型
- 读者-作者模型
- 宴席哲学家
警惕同步方法之间的依赖
- 避免使用一个共享对象的多个方法
- 基于客户端的锁定——客户端在调用方法前锁定服务端
- 基于服务端的锁定——在服务端内创建锁定服务端的方法,给客户端调用
- 适配服务端——创建执行锁定的中间层
保持同步区域微小
很难编写正确的关闭代码
- 尽早考虑关闭问题,尽早令其工作正常。
测试线程代码:不同编程配置、系统配置、负载条件下频繁运行。
- 将伪失败看作可能的线程问题:不要将系统错误归咎于偶发事件
- 先使非线程代码可工作
- 编写可插拔的线程代码
- 编写可调整的线程代码
- 运行多于处理器数量的线程
- 在不同平台运行
- 调整代码并强迫错误发生
- 装置试错代码
- 硬编码:手工插入
wait()``sleep()``yield()``priority()
的调用 - 自动化:使用异动策略搜出错误
- 硬编码:手工插入
- 装置试错代码
第十四章 逐步改进
——对一个命令行参数解析程序的案例研究
只是大致看了一下,在书上看代码全是黑的,不一目了然,太费劲了。