这是一本关注程序细节并具有浓厚实战意义的书籍, 它主要是来告诉我们如何写出容易让人理解的代码. 如果非要在书名前加一个限定词的话, "java实现模式"可能更贴切一些, 因为里面的代码都是以java作为例子来说明的. 作者是junit的贡献者之一, 因此里面的很多例子都是从junit中来的.
这本书也解决了很多自己在实际开发中的一些抉择困惑. 是一本非常值得常看的书籍. 另外翻译的也非常不错, 不像有些书让人看了云里雾里的, 不知所云.
本书按照方法, 行为, 类, 状态, 集合, 框架的顺序依次对相关的实现模式加以说明来阐述如何写出好的代码. 也是本书的重点, 集合主要是在讲java.util下的集合类的使用法则, 框架部分主要是框架的开发有自己的规则, 围绕兼容性, 扩展性方面在说事儿, 没怎么看明白, 可以略过^_^
价值观
沟通, 简单, 灵活(也是实现模式的三个价值观)影响了我在编程时所作的每个决策
价值观有普遍的意思, 但是很难直接使用, 模式虽然可以直接是使用, 但是却针对具体的场景, 原则在二者之间搭建了桥梁.
模式描述了要做什么, 价值观提供了动机, 原则把动机转化成实际行动.
程序的绝大多数开销是在他第一次部署之后才产生的.
应该选择那些提倡灵活并能带来及时收益的模式.对于立即增加成本却收效缓慢的模式, 最好多一点耐心, 先把他们放回口袋, 需要的时候再拿出来.
原则
局部化影响
组织代码结构时, 要保证变化只会产生局部化影响.
最小化重复
将逻辑和数据绑定
把逻辑与逻辑所处理的数据放在一起 , 如果有可能尽量放在一个方法中, 或者退一步, 放在一个对象里面, 最起码也要放到一个包下面. 发生变化时, 逻辑和数据很可能会被同时改动. 如果放在一起, 那么改动他们所造成的影响就会只停留在局部.
对称性
与其说是对称性, 还不如说是代码的一致性, 也就是方法的命名最好能做到一致, 符合人类的认知习惯, 比如有个add方法, 最好能提供个remove方法
不一致的代码:
void process(){
input();
count++;
output();
}
勉强一致的代码:
void process(){
input();
incrementCount(); // 方法名没有体现意图而是在说实现细节
output();
}
最终一致的代码:
void process(){
input();
tally();
output();
}
声明式表达
尽可能用声明式来表达意图.
对那些只是陈述简单事实, 不需要一系列条件语句的程序片段, 如果用简单的声明方式写出来, 读者更容易.
作者举的例子是junit中通过注解来配置放在test suite中测试类.
变化率
把具有相同变化率的逻辑, 数据放在一起, 不同的进行分离
变化率也适应用于数据, 一个对象中所有成员变量的变化率应该差不多是相同的. 两个同时变化但是又和其他成员的变化步调不一致的 可能应该属于某个辅助对象, 比对金融票据中的数值和币种会同时变化.那么这两个字段最好放在辅助对象Money.
变化率原则也是对称性的一种, 只不过是时间上的对称.
类的模式
数据的变化比逻辑的变化要频繁的多, ----我不同意这个观点哈^_^
子类和超类之间的关系应该:我和超类很像, 只是有少许差异, 像那种对超类的某些方法进行覆盖并不是一种好的做法.
简单的超类名
重要的类, 尽量使用一个单数来为他命名
限定性的子类名
子类的名字:不仅要描述这些类像什么, 而且还要说明子类之间的区别是什么, 通常在超类的基础上扩展一两个词就可以得到子类名.
要从阅读者的角度来想想他们需要了解这个类的什么信息.
太长的类名读起来费劲儿, 太短又考验读者的记忆力.
抽象接口
为什么我们不能一口气理出系统中所有需要灵活性的地方呢, 因为需求和技术都在不可预测的变化
对灵活性的需要, 灵活性的成本, 何处需要灵活性的不可预测导致了只有当我们确信无疑需要灵活性时, 才能引入这种灵活性.
有版本的interface
一般情况下, 使用instanceof会降低灵活性, 因为这样会将代码与具体类绑定在一起. 这是一种丑陋的解决方案, 用来解决一类丑陋的问题.
值对象
这个十分巧妙. 至少我还没用过:( 只能看代码理解了, 在财务系统中, 把基本教义表现为不可变的数学值:
class Transaction{
int value;
Transaction(int value, Account credit, Account debit){
this.value =value;
credit.addCredit(this);
debit.addDebit(this);
}
int getValue(){return value;}
}
Transaction一旦创建, 它的值就无法改变.
值对象的所有状态都应该在构造时传入, 其他地方不再提供改变其内部状态的方式. 对值对象的操作总是返回新的对象, 操作的发起者要自己保存返回的对象.
对值对象, 最大的反对意见总是性能:创建那么多临时对象, 会让内存管理系统不堪重荷.
条件语句
条件语句if/else放在一个类中的好处:所有逻辑在一个类中, 阅读者不必四处寻找所有可能的计算路径. 缺点是:除了修改对象本身的代码之外, 没有其他办法修改它的逻辑.简而言之, 条件语句的好处在于简单和局部化, 如果用的太多, 这种好处就会变成弱点.
委派
解除条件语句的魔咒之一就是采用委派, 而委派的一个常用技巧:把发起委派的对象作为参数传递给接收委派的方法, 例子为证:
public class GraphicEditor {
private RectangleTool tool;
public void mouseDown() {
tool.mouseDown(this);
}
public void add(RectangleFigure rectangleFigure) {
}
}
public class RectangleTool {
public void mouseDown(GraphicEditor editor) {
editor.add(new RectangleFigure());
}
}
匿名内部类
要用好匿名类内部类, API就必须极其简单, 或者有一个超类实现了绝大部分需要的方法, 总之匿名类应该尽量简单就对了. 这样也是为了避免扰乱阅读者的视线.
状态的模式
有效管理状态的关键在于:把相似的的状态放在一起, 确保不同的状态彼此分离.
变量
变量的类型由类型声明来表述就够了, 为此应该确保声明时的类型尽量清晰的描述变量的用途
如果作用域, 生命周期和类型都能用别的方式充分描述, 名称本身就可以只用于描述变量在计算逻辑中扮演的角色. 把需要承载的信息减少到最少.
常量
如果一个方法在调用时必须传入常量作为参数(比如setJustification(Justification.CENTERED)), 总可以针对每个常量值单独建立一个方法(比如justifyCentered()), 从而更好的表达你的意图.
按角色命名
一般而言, 名字被读到的次数比写出的次数要多的多, 所以在起命名时应该更重视可读性, 而不是输入的便利性.虽然使用缩写词的确可以让输入更快, 但是这个是以降低可读性为代价的.
在变量名中要表达的最主要的信息就是变量的角色, 这也让我们能够清晰简洁的命名, 如果命名遇到困难, 通常是因为我们还没有充分理解当前的计算逻辑
行为的模式
主体流
要清晰的表达程序的主体流, 用异常和防卫语句去表达不寻常的或者错误的情形.
消息
java中表达逻辑的主要手段是消息, 过程性语言使用过程调用来作为信息隐藏的机制.
对于
compute(){
input();
process();
output();
}
这样的过程性语言来说, 他的细节包含在步骤之中, 在我们理解compute()方法时不需要关心.而对于对象语言来说, 细节隐藏在作为消息的对象之中.理解了这一点, 就可以尽可能清晰和直接地表达逻辑, 而适当的推迟牵涉到的细节.
选择性消息
据个例子, 对于打算在若干个方法中显示一个图形, 可以发送一条多态的消息来传达出"选择将在运行时发生"的消息.
void display(Shape subject, Brush brush){
brush.display(subject);
}
这里的brush和shape都是可以 进行选择的, 这样就可以进行自由的组合.
广泛的使用选择性消息可以使代码很少出现明确的条件语句, 每条选择性消息都是对未来扩展的一个邀请.
选择性消息的一个缺点就是阅读者可能要看好几个类才能理解一条特殊执行路径的细节.
反置性消息
本来是调用辅助类提供的方法来完成逻辑:
compute(){
input();
helper.process();
output();
}
这个写法的问题在于代码不够一致(对称性), 不美观. 反转一下, 并将input(), output()挪到helper中:
compute(){
new Helper(this).compute();
}
Helper:
compute(){
input();
process();
output();
}
解释性消息
主要说了两个, 一个是方法的命名要尽量说明意图, 而不是实现, 另外一个如果一行代码需要注释, 那么用方法封装之, 因为方法就是消息的载体.
异常流
应该尽可能清晰的表达主体流, 并在不模糊主体流的前提下尽可能清晰的表达这些异常路径.
顺序执行的程序是最容易阅读理解的.
卫述句
卫述句也就是短路检查语句.比如经过判断之后return, continue等
已检查异常
当抛出异常的程序和捕获异常的程序由不同的人编写实现, 抛出的异常未被捕获的风险更大. 因此java提供了已检查异常(非runtime exception)
方法的模式
方法的命名也是与阅读者沟通的机会.它可以告诉阅读者这段计算的目的何在, 让阅读者免受实现的影响.
揭示意图的名称
应该从潜在的调用者的想法出发, 根据调用者使用该方法的意图来给方法命名. 至于方法的实现策略可以通过另外的途径去传达.除非实现策略对用户有意义, 否则应该将其冲方法名种拿掉.
调用代码是在讲述一个故事, 好的方法名会让 讲述得更流畅.
重载方法
多个重载方法的目的应该一致, 不一致的地方应仅限于参数类型. 如果重载方法的返回类型不同, 会让代码难以理解, 最好为新的意图找一个新的名字, 不同的计算应该有不同的名称.
方法注释
对于沟通良好的代码来说, 很多注释完全是多余的. 编写这些注释, 以及维护这些注释与代码一致性的代价, 远高于他们带来的价值.
沟通仍然是所有实现模式的首要价值, 如果方法注释是最合适的沟通媒介, 那么写一个好注释吧.
转换方法
如果需要表达类型相近的对象之间的转换, 且转换的数量有限, 那么把转换表达成源对象的一个方法. 一般这样命名: asXxxx()
转换方法的缺点在于引入了源对象到目标对象之间的依赖关系, 如果原先不存在这样的依赖关系, 而仅仅为了转换方法而引入依赖是不值得的.另外提供过多的转换方法也是不合适,
转换构造器
他很适合用于将一个源对象转换成许多目标对象, 这样转换就分布在各个目标对象中.比如File(String), URL(String)等构造器是合适的, 如果提供String.asFile()就不合适了.
完整的构造器
构造器将客户绑定到一个具体类, 调用构造器意味着你愿意使用一个具体类, 如果你希望更抽象, 应该使用工厂方法.
工厂方法
如果要完成的工作比单纯的创建对象更复杂, 比如在缓存中记录对象, 那么工厂方法是合适的, 工厂方法一般都会引起阅读者的好奇, 想探究一下里面到底做了什么, 否则就不应该误导阅读者的这种好奇.
内部工厂
对于getX()方法来说, 如果x的计算很复杂, 那么需要将该过程封装到一个内部工厂方法中
getX(){
if(x == null){
x = computeX();
}
return x;
}
容器访问方法
如果容器不允许修改, 可以将容器的拷贝传递出去之外, 也可以将容器包装成一个不可修改的容器, 缺点之处在于修改的时候会引发不优化的隐藏.
另外一个是将容器方法返回一个将remove()方法throw UnsupportedOperationException()的一个Iterator
查询方法
如果一个对象有很多逻辑都依赖于另一个对象的状态, 可能意味着逻辑放错了地方, 比如:
if (x.isOK()){
x.invoke1();
}else
x.invoke2();
这里可能x缺少了一个方法而导致对外暴露isOK(), invoke1(), invoke2()三个方法.
setter方法
setting方法是一个根据实现来命名的, 而不是意图. 如果用来给某个字段设置值比较合适, 但是如果是其他的目的则就应该另外考虑. 最好是能了解客户设置这个值是为了解决什么问题.
容器的实现模式
编程中最基本的变化种类之一就是数量的变化.
Set
Set有三种主要的实现:HashSet, LinkedHashSet和TreeSet(实现了SortedSet接口), HashSet速度最快, 但其中的元素是无序排列的. LinkedHashSet按照元素被加入容器的顺序来对元素排序, 但代价是添加删除元素要消耗30%的时间. TreeSet用Comparator来保护元素的顺序, 不过在添加删除元素或者检查元素是否存在时, 所花时间为logn, n为容器大小
查询
折半搜索只有对那些随机访问耗费时间为常数的列表才能起到提升性能的作用, 如ArrayList.
单元素容器
如果想把一个元素传到一个期望传入容器的借口中, 可以通过调用Collections.singleton()来进行转换, 这个方法会返回一个Set, 也有List, Map的版本, 只是这些容器都是不可修改的.