实现模式 (Kent Beck 著)
第1章 引言 (已看)
第2章 模式 (已看)
第3章 一种编程模式 (已看)
第4章 动机 (已看)
第5章 类 (已看)
第6章 状态 (已看)
第7章 行为(已看)
第8章 方法 (已看)
第1章 引言
1.1 导游图
1.2 那么,现在
第2章 模式
绝大多数程序都遵循一组简单的法则
- 更多的时候,程序是在被阅读,而不是被编写
- 没有“完工”一说,修改程序的投入会远大于最初编写程序的投入
- 程序都由一组基本的语句和控制流概念组合而成
- 程序的阅读者需要理解程序----既从细节上,也从概念上。有时他们从细节开始,逐渐理解概念,有时他们从概念开始,逐渐理解细节
使用模式有时会让你感到束手束脚,但确实可以帮你节省时间和精力
第3章 一种编程模式
3.1 价值观
3.1.1 沟通
3.1.2 简单
3.1.3 灵活
3.2 原则
3.2.1 局部化影响
3.2.2 最小化重复
3.2.3 将逻辑与数据捆绑
3.2.4 对称性
3.2.5 声明式表达
3.2.6 变化率
3.3 小结
第4章 动机
第5章 类
5.1 类
学会如何用类来包装逻辑和如何表达逻辑的变化,这是有效使用对象编程的重要部分
5.2 简单的超类名
找到一个贴切的名字是编程中最令人开心的时刻之一
在所有的命名当中,类的命名是最重要的。类是维系其他概念的核心。一旦类有了名字,其中操作的名字也就顺理成章了。相反的情况却很少成立。
对于重要的类,尽量用一个单词来为它命名
5.3 限定性的子类名
子类的名字有两重职责,不仅要描述这些类像什么,还要说明它们之间的区别是什么
子类的名字在交谈中用得并不频繁,所以值得以牺牲简明来换取更好的表现力。通常在超类名的基础上扩展一两个词就可以得到子类名
应该用类名来讲述代码的故事
5.4 抽象接口
请牢记软件开发的古训:针对接口编程,不要针对实现编程
这里所说的“接口”是指“一组没有实现的操作“。
尽管我们成天都在抱怨软件不够灵活,但很多时候系统根本不需要变得更灵活。不管是要进行基础性的修改(例如改变整数类型的字节数)还是大范围的修改(例如引入新的商业模型),大部分软件都不会需要最大限度的那种灵活性
5.5 interface
要用Java表达“这是我要完成的任务,除此之外的细节不归我操心“,可以声明一个interface
interface是java率先引入编程语言市场主流的重要创新之一。
interface是一个很好的平衡,它带来了多继承的一部分灵活性,同时又没有多继承的复杂性和二义性。
一个类可以实现多个interface。interface只有操作,没有成员变量,所以它们能够有效保护其使用者不受实现变化的侵扰
给interface命名有两种风格,选择哪一种取决于如何看待它们,如果把interface看作“没有实现的类“,那么就应该像给类命名一样地给他们命名
“抽象的接口”究竟是interface还是超类倒是不那么要紧
但有时候比起隐藏“此处使用interface“这一事实来,具体类的命名对于交流更加重要。在这种情况下,可以给interface的名字加上“I”前缀
5.6 抽象类
在Java中区分抽象接口与具体实现的另一种方式是使用超类。超类是抽象的,因为超类的引用可以在运行时替换为任何子类的对象;至于这个超类在Java的语法意义上是不是抽象的,这并不重要
何时应该使用超类,何时应该使用interface?取舍最终归结为两点:接口会如何变化,实现类是否需要同时支持多个接口
interface和类继承体系并不是互斥的。你可以提供一个接口说“你可以使用这些功能“,再提供一个超类说“这是一种实现方式“
5.7 有版本的interface
interface能很好的地适应实现的变化,却不容易适应自身结构的变化。尽管如此,interface----和所有设计决策一样----还是有可能会变化,毕竟我们都是在实现和维护的过程中学会设计的
interface Command { void run(); } interface ReversibleCommand extends Command { void undo(); } Command recent = ""; if (recent instanceof ReversibleCommand) { ReversibleCommand downcasted = (ReversibleCommand)recent; downcated.undo(); }
一般情况下,使用instanceof会降低灵活性,因为这会把代码与具体类绑定在一起。但在这个例子里使用instanceof应该是合理的,因为这样才能对interface作出调整
5.8 值对象
要实现一个值对象(或者说,看起来像是整数,而不是像一个可变状态的容器那样的对象),需要首先在"状态的世界“和“值的世界”之间画出一条边界
在上面的例子中,Transaction是值,而Account则包含可变的状态。值对象的所有状态都应该在构造中传入,其他地方不再提供改变其内部状态的方式。对值对象的操作总是返回新的对象,操作的发起者要自己保存返回的对象
5.9 特化
清晰地描述计算过程中的相似性与差异性的相互作用,可以让程序更容易阅读,使用和修改。在实际工作中,没有哪段程序是独一无二的。不同程序会表达相似的概念。同样,一个程序中的很多部分往往也表达着相似的概念。清晰地描述相似性和差异性,就能让阅读者更好地理解现有的代码,找出自己想要做的事情是否已经被某种现有的各种实现所覆盖,以及----如果还没有现成实现----如何对现有代码加以特化或是编写新的代码以满足需要
最简单的变化是状态的差异,字符串“abc“显然是与“”def“不同的,但操作这两个字符串的算法是一样的
最复杂的变化是在逻辑上完全不同。符号积分子程序和数学式排版子程序在逻辑上毫无共通之处,虽然它们接受的输入可能是一样的
大多数程序位于两个极端----"相同的逻辑处理不同的数据“和“不同的逻辑处理相同的数据“----之间的某个位置
就连逻辑和数据之间的分界也有些模糊:一个标记,它本身是boolean型的数据,但会影响控制流的运行;一个辅助对象可以被保存在成员变量里,但它又可以对计算的过程造成影响
5.10 子类
声明一个子类就是在说,这些对象与那些对象很相似,只除了......如果有个适当的超类,创建子类会是一种强大的编程方式。通过覆盖适当的方法,只需几行代码就可以为现有的计算逻辑引入变化
对象技术刚开始流行时,继承一度被视为万灵药。一开始人们用继承来分类:Train是Vehicle的子类,不管两者是否共享任何实现。不久,一些人又发现:既然继承所做的就是共享实现,用它来抽取共同的实现部分就是最有效的办法了。但好景不长,继承的局限性很快就暴露出来了。首先,这张牌只能用一次:如果事后发现一些变化情况无法用子类的方式很好地表达,就得先花点工夫把代码从继承关系中解开,然后才能重新组织它。其次,使用者必须首先理解超类,然后才可能理解子类,随着超类变得复杂,这个问题会更加严重。第三,对超类的修改颇有风险,因为子类有可能依赖于超类实现中某个微妙的属性。最后,在过深的继承体系中,所有这些问题都会出现
创建平行的继承体系可以算是特别糟糕的继承用法:“这个”继承体系中的每个子类都需要“那个”继承体系中的一个对应的子类。这既是重复的一种形式,又在类继承体系之间建立了隐晦的耦合。以后如果要引入一种新的变化情况,就要同时修改这两个继承体系。
只要记着所有这些警告,子类继承还是一种用于表达计算的“基调与变奏”的强大工具,合适的子类能帮助人们用一两个方法准确地描述出自己想要的计算逻辑,要得到合适的子类,关键在于把超类中的逻辑进行彻底地划分,直到每个方法只做一件事。在编写子类时,应该可以只覆盖一个方法而不管其他方法。
5.11 实现器
在由对象组成的程序中,多态消息是表达选择的基本方法之一。为了让消息能起到选择的作用,能够接收到该消息的对象就必须不止一种
把同一个协议实现多次(不管是用implements语法实现一个interface,还是用extends语法继承一个类)所表达意思是:从计算的这一方面来看,只要有某些符合代码意图的事情发生就可以了,至于“到底发生了什么“,我们并不关心
多态消息的优美之处在于,它们给系统开启了变化的机会。如果程序中的某一部分要把一些字节写到另一个系统,引入重复的Socket就能让开发者随时改变套接字的具体实现而不影响其使用者。相比实现同样功能的过程式实现(明确而封闭的条件逻辑),对象/消息的实现方式更加清晰,并且分离了意图(传递一组字节)与实现(用某些参数来进行TCP/IP调用)。同时,用对象和消息的方式描述计算,让系统有可能以最初的程序员做梦都想不到的方式发生变化。清晰的表达与灵活性,两者的天作之合正是面向对象语言成为主流的原因所在
5.12 内部类
有时候需要把一部分计算逻辑包装起来,但又不想新建一个文件来安置全新的类。这时可以声明一个小的私有类(内部类),这样就可以低成本地获得类的大部分好处
有时内部类只需要继承Object。有时它们会继承别的超类,从而告诉阅读者:超类的这种细微变体只在这个小范围内有意义
5.13 实例特有的行为
从理论上来说,一个类的所有实例逻辑都是一样的。如果放松这一限制,就会出现一些新的表达方式,但这些方式都有其成本。如果对象的逻辑完全由类来决定,阅读者只要看类中的代码就知道会发生什么。一旦各个实例有不同的行为,就需要在运行时观察或者分析数据流才能理解一个对象的行为
如果实例的逻辑会在计算进行的过程中发生改变,那么它带来的成本会更高。为了让代码更容易被读懂,即便是实例特有的行为,也最最好是在对象创建之初就确定下来,之后不再改变
5.14 条件语句
要实现实例特有的行为,if/then和switch语句是最简单的方式。使用条件语句,不同的对象会根据其中的数据来执行不同的逻辑。这种表达方式的好处是:所有逻辑仍然在同一个类里,阅读者不必四处寻找所有可能的计算路径。但条件语句的缺点是:除了修改对象本身的代码之外,没有其他办法修改它的逻辑
把条件逻辑编程消息,发送给子类或者委派(哪种方式更好取决于代码的具体情况)。如果条件逻辑出现重复,或者各个条件分支上的逻辑差异很大,那么用消息的方式来描述会比显式的条件逻辑更好。此外,频繁变化的条件逻辑也最好是用消息来实现,这样可以使各个分支的修改更简单,对其他分支的影响更小
简而言之,条件语句的好处在于简单和局部化。如果用得太多,这些好处反而变成了弱点
5.15 委派
要让不同的实例执行不同的逻辑,另一种办法是把部分工作委派给不同类型的对象:不变的逻辑放在发起委派的类中,变化的逻辑交给被委派的对象
public void mouseDown() { getTool().mouseDown(); }
从前放在各个switch子句中的代码被搬到了不同的工具子类中
使用委派有一个常用技巧:把发起委派的对象作为参数传递给接受委派的方法
GraphicEditor public void mouseDown() { tool.mouseDown(this); } RectangleTool public void mouseDown(GraphicEditor editor0 { editor.add(new RectangleFigure()); }
5.16 可插拔的选择器
假设我们需要实例特有的行为,但只需要在一两个方法中体现,并且你也并不介意把各种变化情况的代码都放在一个类里。在这种情况下,可以把要调用的方法名保存在实例变量中,然后通过反射来调用该方法
String name; public void runTest() throws Exception { Class[] noArguments = new Class[0]; Method method = getClass().getMethod(name, noArguments); method.invoke(this, new Object[0]); }
5.17 匿名内部类
5.18 库类
如果某些功能放在哪个对象中都不合适,那么该把它们放在哪里呢?一个办法是在一个空类中创建静态方法。任何人都不应该创建这个类的实例,它只是用来安放这些功能
尽管库类相当常见,但它不适合大量使用。把所有逻辑都放在静态方法中就错过了对象的最大坏处:把数据放入私有命名空间以便简化逻辑。应该尽量把库类变成合格的对象
5.19 小结
第6章 状态
6.1 状态
把世界看作许多不断变化的事物的总和,这种思路早已被人们证明行之有效
对象语言也采用了处理状态的策略。这些语言把系统的状态细分到各个小块中,每个小块对其他小块的访问都受到严格限制
有效管理状态的关键在于:把相似的状态放在一起,确保不同的状态彼此分离。有两条线索指出两个状态的相似性:它们在同一个计算中被用到,它们出现和消亡的时间相同。如果两个状态总是被同时使用,而且有同样的生命周期,它们就应该被放在一起
6.2 访问
编程语言中的二分法之一,就是对“访问存储值“和“执行计算“的区分。实际上两个概念是可以互通的:访问内存状态相当于调用一个函数,后者返回当前存储的值;调用函数相当于读取一个内存位置,并对其中的内容计算(而不是简单的返回)
6.3 直接访问
要表达“我在读取数据”或者“我在存储数据“,最简单的方式就是直接访问变量
x = 10
这种做法的好处在于表达清晰,但清晰的代价是损失了灵活性。如果在多个地方对同一个变量赋值,那么在需要作出改变时就可能必须修改这些地方
直接访问的另一个缺点是:这种操作属于实现细节,其层面低于编程时通常的思考层面。比如说,把某个变量设为1的效果可能是打开车库的大门,但这种反映实现细节的代码无法有效地讲述我真正的意图
doorRegister = 1
openDoor()
door.open()
编程时通常的思考层面与存储无关。如果程序中到处都是直接访问变量的代码,就会给沟通造成障碍
人们找出了一些规律:只在访问器方法(也许还可以加上构造器)中直接存储,只在类及其子类(也许还可以扩大到类所在的包)中使用直接存储。
这些都不是绝对的规则,程序员需要不断思考,交流和学习。毕竟,这是成为专业程序员的必经之路
6.4 间接访问
可以用方法调用来隐藏对状态的访问和修改。这些访问器方法能带来更好的灵活性,但同时也付出了降低清晰直观程度的代价
对于“如何访问状态”,我的默认策略是,允许在类(及其内部类)中直接访问,其他的使用者必须间接访问
另一种策略是完全禁止直接访问,只允许间接访问
6.5 通用状态
很多计算逻辑会涉及同样的数据项,尽管其中的值可能不同。如果发现这样的一组计算逻辑,为了表达意图,应该把它们共同的数据项声明为一个类中的字段
另一种可选的方案是可变状态:同一个类的对象可能有不同的数据项
一个对象中所有的通用状态应该具有同样的作用域和生命周期。有时我被诱惑着引入一个这样的字段:它只被对象中的一小部分方法使用,或者只在某个方法被调用的过程中有效。每当遇到这种情况,我总能找到一个更好的地方来保存这部分数据(可能是一个参数或者一个辅助对象),从而改善代码质量
6.6 可变状态
有时取决于不同的使用方式,同一个对象需要不同的数据元素----不仅是数据值改变,就连对象中的数据元素也全然不同,尽管这些对象都来自同一个类
可变状态通常用map来保存,其中的键(key)是数据元素的名字(表现为字符串或者枚举类型),值(value)则是数据值
class FlexibleObject { Map<String, Object> properties = new HashMap<String, Object>(); Object getProperty(String key) { return properties.get(key); } void setProperty(String key, Object value) { properties.set(key, value); } }
可变状态比通用状态要灵活得多,它最大的问题是表意不清晰:对于一个只有可变状态的对象,需要哪些数据项才能让它正常工作?只有仔细阅读代码,甚至观察程序执行之后,才能回答这个问题
如果遇到一个字段的状态决定了同一个对象中是否需要其他字段,这种情况下就应当使用可变状态
要尽量使用通用状态,只有当是否需要某些字段视使用方式而定时,才考虑使用可变状态
6.7 外生状态
对于特殊用途的信息,应该保存在使用该信息的地方,而不是保存在对象内部
外生状态的缺点之一是对象复制会变得困难。如果对象具有外生状态,复制它就不仅仅是“复制所有字段“那么简单,还必须确保所有外生状态都被正确地复制,对于不同用途的外生状态,可能需要做不同的处理。另一个缺点是有外生状态的对象难以调试,普通的状态检视工具无法列出与对象相关的外生状态。由于这些难题,外生状态并不常见,但在真正需要时还是很有用的
6.8 变量
代码的阅读者需要知道变量的作用域,生命周期,角色和运行时类型
6.9 局部变量
局部变量常扮演的角色有几种
(1)收集器(Collector)
用变量来收集稍后需要的信息。收集器的内容经常会作为返回值传出
(2)计数(Count)
这是一种特殊的收集器,专门用于记录某些其他对象的个数
(3)解释(Explaining)
如果有一个复杂的表达式,可以把表达式的一部分结果赋值给一个局部变量,从而帮助阅读者理解整个复杂的运算
int top = ...; int left = ...; int height = ...; int bottom = ...; return new Rectangle(top, left, height, width);
从计算的角度来说,解释性局部变量并没有存在的必要,但它们可以帮助人们理解复杂的运算逻辑
解释型局部变量往往可以再向前走一步,变成辅助方法,表达式变成方法体,局部变量的名字则是给方法命名的线索。有时引入这样的辅助方法只为简化主方法,有时它们还可以消除类似的表达式中的重复代码
(4)复用(Reuse)
long now = System.currentTimeMillis(); for (Clock each: getClocks()) each.setTime(now)
(5)元素(Element)
for (Clock each: getClocks())
6.10 字段
字段的作用域和生命周期与其所属的对象相同
下面列出了字段扮演的一些角色。这个列表不像前面局部变量的列表那么全面,只是列出了字段常扮演的几种角色
- 助手(Helper)助手字段用于存放其他对象的引用,该对象会被当前对象的很多方法用到。如果有一个对象以参数的方式传递给很多个方法,就可以考虑改为通用助手字段(而不是参数)获得所需的对象,并在构造函数中给助手字段赋值
- 标记(Flag)boolean型的标记表示“这个对象可能有两种不同的行为方式“。如果这个标记再有setter方法,那就表示“....而且行为可能在对象生命周期中发生改变“。如果只是用来表示不多的几种条件,标记字段并没有什么问题。如果根据某个标记作出判断的逻辑出现了重复,则应该考虑改为使用策略字段
- 策略(Strategy)如果想要表达“这部分计算有几种不同的方式来进行“,就应该把一个“只执行这部分可变的计算“的对象保存在一个字段中。如果计算方式在对象生命周期中不发生变化,就在构造函数中给策略字段赋值,否则就提供一个方法来改变策略字段的值
- 状态(State)状态字段和策略字段有相似之处,它们所在的对象都会把一部分行为委派给它们。但状态字段在被触发时会自己设置相关的状态,而策略字段即便会发生改变,这改变也是由其他对象来进行的。用状态字段实现的状态机会很难理解,因为状态和变迁不在同一个地方描述。不过如果状态机足够简单,那么用状态字段来实现也够了
- 组件(Components)这样的字段用来保存由所在对象“拥有”的对象或者数据
6.11 参数
除了非私有的变量(字段或者静态字段)之外,要把状态从一个对象传递到另一个对象就只能通过参数了。由于非私有字段会在类与类之间造成强耦合,而且这种耦合会与日俱增,所以只要可能,就应该尽量使用参数来传递状态
比起从一个对象永久地引用另一个对象,参数带来的耦合要弱得多。比如,在树型结构中进行的计算有时需要用到节点的父节点。此时不应该让节点直接引用自己的父节点,而是应该以参数的形式把后者传递给需要它的方法,从而弱化节点之间的耦合。举例来说,如果没有指向父节点的永久引用,就可以让一颗子树同时属于几颗树
如果一个对象给另一个对象发送的很多消息都需要同一个参数,那么也许更好的办法是把这个参数永久地交给被调的对象
6.12 收集参数
6.13 可选参数
6.14 变长参数
6.15 参数对象
如果同一组参数被放在一起传递给了很多方法,就应该考虑创建一个对象,把这些参数放入该对象的字段,然后传递这个对象
6.16 常量
常量的名字通常全部大写,以强调它们不是普通的变量
6.17 按角色命名
在变量名中使用缩写词,降低可读性
6.18 声明时的类型
6.19 初始化
6.20 及早初始化
6.21 延迟初始化
6.22 小结
第7章 行为
7.1 控制流
将运算表达成一系列的步骤
7.2 主体流
明确表达控制流的主体
7.3 消息
Java中表达逻辑的主要手段是消息
compute() {
input();
process();
output();
}
用消息作为基本的控制流机制等于承认了变化是程序的基本状态。每个消息都是一处消息接收者可能发生变化而发送者不变的潜在场所。所以这种以消息为基础的过程说的不再是“那里有些东西,不过它的细节不重要“,而是,“在这里发生的情节和输入有关,不过其中的细节有可能变化“。明智地运用这种灵活性,尽可能清晰和直接地表达逻辑,并适当地推迟牵涉到的细节,如果想编写出能有效传达信息的程序,这是一种重要的技巧
7.4 选择性消息
有时候发送消息去选择一个实现,这和过程性语言中使用case语句很相似
public void displayShape(Shape subject, Brush brush) { brush.display(subject); }
广泛使用选择性消息可以使代码很少出现明确的条件语句。每条选择性消息都是对未来扩展的一个邀请。而当你打算修改整个程序的行为时,每一处明确的条件语句都是又一个必须费神的修改点
阅读大量使用了选择性消息的代码需要技巧。选择性消息的一个代价是阅读者可能要看好几个类才能理解一条特定路径的细节
7.5 双重分发
7.6 分解性(序列性)消息
对于一个由很多步骤组成的复杂算法,有时候可以把相关的步骤组合到一起,然后发送一条消息去调用它们。这个消息的目的不是提供一种特殊化的手段,或者什么深奥的东西,它只是平凡的功能分解。消息在这里单纯是为了调用例程中的一些步骤组成的子序列
分解性消息需要有描述性的名称。要让大多数阅读者仅从名字就能够得知该子序列的意图。只有那些对实现细节感兴趣的人才有必要区阅读分解性消息所调用的代码
7.7 反置性消息
一旦培养其对代码之美的嗅觉,从代码中得到的美感将是对代码质量的一种宝贵的反馈。这些从象征性思维层面之下涌出的感觉,其价值一点都不逊于经过充分证明并明确命名的模式
7.8 邀请性消息
有时当你写代码的时候,会预期其他人将在子类中变动其中一部分运算。此时应发送适当命名的消息,去传达这种将来进行改进的可能性,这样的消息是在邀请程序员今后按照他们自己的意图去调整运算
如果逻辑存在一个默认的实现,那么可令其成为消息的实现。 如果不存在,那么可令方法成为抽象的,以便明确该邀请
7.9 解释性消息
意图与实现之间的区分在软件开发中总是很重要的。它让你得以首先理解运算的要旨,如果有必要,再进一步理解其细节。可以用消息来彰显这种区分,现发送一条“以要解决的问题来命名“的消息,这条消息再发送“以问题如何被解决来命名“的消息
highlight(Rectangle area) {
reverse(area);
}
highlight()没有起到运算上的作用,但它尽到了传达意图的职责
7.10 异常流
程序除了有主体流,还有若干异常流。这些是在沟通上不那么重要的执行路径,因为它们较少执行,较少变化,或者在概念上次于主体流。应该清晰地表达主体流,并在不模糊主体流的前提下尽可能清晰地表达这些异常路径,卫述句和异常是表达异常流的两种方式
7.11 卫述句
即使程序有一个主体流,有些情况下也会需要偏离它。卫述句是一种表达简单和局部的异常状况的方式,它的影响后果完全是局部的
void initialize() { if (!isInitialized()) { ... } } void initialize() { if (isInitialized()) return; ... }
第一个版本在我读到then字子句的时候,还要提醒自己记得去找else子句。我要在头脑中想象一个栈来放置这些条件。第二个版本的前两行直接提醒我注意一个事实:接收者还没有初始化
If-then-else表达的是可供选择的多个同样重要的控制流。卫述句更适合表达另一种情形,即其中一个控制流比其他的更重要
7.12 异常
当程序中有的流程跳转跨越了多个层次的函数调用时,用异常来进行表述会让逻辑更加清晰。如果意识到调用栈上面有一层发生了问题,例如磁盘满了或者网络连接中断了,可能要往下好多层才能合理地处理这件事。在发现情况的点跑出异常,并在可以处理该情况的点捕捉异常,比起到处插入检查代码要好多了,那样不仅要明确地检查所有可能的异常条件,而且一个也处理不了
异常有代价,它是设计漏洞的一种表现。被调用的方法会抛出异常这一事实影响了所有可能调用它的方法的设计和实现,一直到调用方法捕获了异常为止
7.13 已检查异常
Java准备了检查异常,它是由程序员显式地声明,并由编译器进行检查的,受到已检查异常影响的代码必须要么捕捉异常,要么讲它传递下去
7.14 异常传播
异常发生在各个抽象层次
低层异常通常包含一些对分析问题有价值的信息,用高层的异常去包装低层的异常,这样当异常信息输出到比如日志的时候,能记下足够的信息来帮助寻找错误
7.15 小结
第8章 方法
8.1 组合方法
通过对其他方法的调用来组合出新的方法,被调用方法应该大致属于相同的抽象层次
抽象层次的混杂预示着糟糕的组合
void compute() { input(); flags != 0x0080; output(); }
组合方法时应当根据事实而非推测。先让代码正常工作,然后再决定该怎么安排它的结构
8.2 揭示意图的名称
应该从潜在调用者的想法出发,根据调用者使用该方法的意图来给方法命名。你可能还想在方法名称中传达其他信息,比如方法的实现策略。不过,最好只在名称中传达意图,方法的其他信息可以通过另外的途径去传达
代码作者的目标不应该是马上就把程序所有信息都一股脑地倒出来,有时候保持克制是必要的。除非实现策略对用户有意义,否则应该把它从方法名称中拿掉
8.3 方法可见性
方法可见性有两大约束条件。既要向外部用户暴露出一些功能,又要保持未来的灵活性
在选择可见性的时候要在两件事情之间进行权衡。一是未来的灵活性,狭窄的接口更便于未来的变化。二是调用对象的代价,过于狭窄的接口让对象的所有客户都被迫执行更多不必要的工作。二者的平衡是决定可见性时要考虑的核心问题
public,package,protected,private
8.4 方法对象
要是一个方法里的代码逻辑纠缠不清,像是硬塞到一起的,方法对象能帮助你将它整理成可读的,清晰的,逐层向阅读者揭示其细节的代码
8.5 覆盖方法
超类中的抽象方法明确地邀请实现者对一段计算进行特殊化
超类中经过良好组织的方法提供了大量的潜在替换点,供你换上自己的代码
8.6 重载方法
用不同的参数类型声明同一个方法,所表达出来的意思是“这个方法的参数有多种格式”
多个重载方法的目的应该一致,不一致的地方应仅限于参数类型。如果重载方法的返回类型不同,会让代码难以理解。最好为新的意图找一个新的名字,不同的计算应该有不同的名称
8.7 方法返回类型
在程序的演变过程中,返回类型是一个经常发生变化的地方
8.8 方法注释
归根结底,沟通仍然是所有实现模式的首要价值,如果方法注释是最合适的沟通媒介,那就写一个好注释吧
8.9 助手方法
助手方法是组合方法的衍生产物。要想将大方法分割成若干小方法,就少不了这些小小的助手方法。助手方法的目的是通过暂时隐藏目前不关心的细节,让你得以通过方法的名字来表达意图,从而令大尺度的运算更具可读性。助手方法的一般声明为private,如果打算允许子类进行微调,可以提升为protected
助手方法的最终目标是将共通的小片段合而为一。如果每次需要某个特定的计算片段,都去调用同一个助手方法,那么修改起来就很容易。如果任由两三行代码一再重复,你不但丧失了通过精心选择的方法名传达其意图的机会,修改起来也很困难
8.10 调试输出方法
很多情况下都有必要讲对象表示成字符串
投入精力实现高质量的调试输出能得到很好的回报
8.11 转换
转换模式的目标也是清晰地传达程序员的意图
8.12 转换方法
如果需要表达类型相似的对象之间的转换,且转换的数量有限,那么应该把转换表达成源对象中的一个方法
class Polar { Cartesian asCartesian() { ... } }
8.13 转换构造器
转换构造器把源对象当做参数输入,然后返回目标对象。转换构造器很适合用于将一个源对象转换成许多目标对象,因为转换分散在各个目标对象里,不会全部堆积在源对象的代码中
例如,File类的转换构造器可以将代表文件名的String对象转换成能执行读取,写入,删除等操作的文件对象。虽然有个String.AsFile()也很方便,但这样一来String类包含的转换数量就止境了。因此还是File(String name),URL(String spec)和StringReadStream(String contents)更好一点
8.14 创建
人们发现了一个难看的事实:编写出来的程序不仅是用来运行的,还是用来修改的
小的程序一般比大的程序容易修改。将运行大程序的大计算机拆分成一群更小的计算机(对象),能使程序更容易修改,人们很早就采用了这样的策略。对象很好地满足了未来修改的要求,它提供了一个事件视界,在视界之内,程序的修改成本是很低的
必须在清晰而直接的表达与灵活性之间取得平衡,才能有效地利用对象创建来表达信息
8.15 完整的构造器
构造器讲客户绑定到了一个具体类。调用构造器意味着你愿意使用一个具体类
8.16 工厂方法
对象创建的另一种表达方式是通过类中的一个静态方法来表达。这种方法比起构造器有几个优势:可以返回更抽象的类型(某个子类或接口的某个实现),还可以根据方法的意图来命名,不必与类名一致。不过工厂方法增加了复杂性,因此只有在它们的优势的确有意义的时候,才应该使用工厂方法,而不要把工厂方法看作是理所当然
Rectangle.create(0, 0, 50, 200);
如果要完成的工作比单纯创建对象更复杂,比如要在缓存中记录对象,或者在运行时决定创建哪一个子类的对象,工厂方法就很合适
8.17 内部工厂
如果要在内部创建一个辅助对象,但创建过程很复杂,或者希望让子类能够修改创建逻辑,应该怎么做呢?设立一个方法负责创建并返回新对象
延迟初始化经常用于内部工厂中,Getter方法经常意味着变量的延迟初始化
getX() { if (x == null) x = ...; return x; }
内部工厂同时也是对子类的邀请,请子类根据需要进行微调
8.18 容器访问器方法
如果对象里包含了一个容器,那么应该为它提供什么样的访问方式呢?
更好的办法是提供一些方法,为容器中的信息提供限制性的,意义明确的访问途径
void addBook(Book arrival) { books.add(arrival); } int bookCount(){ return books.size(); } Iterator getBooks() { return books.iterator(); }
如果你发现自己需要重复实现容器的很多行为,那么很可能问题出在设计上。如果对象能为用户多做一点事情,它就不必对外暴露那么多内部构造了
8.19 布尔值Setting方法
8.20 查询方法
有时候对象需要根据另一个对象的状态来做决定。这是一种不理想的情况,因为一般来说其他对象应该为它们自己做决定。不过万一某对象确实需要对外提供决策依据,那么相应的方法名称应该加上“be” 或“have”的某种形式(如“is”或者“was”)作为前缀
8.21 相等性判断方法
Equals()和hashCode()就是残留到今天的遗迹,要用它们就必须遵守规则,否则将遭遇到奇怪的错误,比如把一个对象放进集合却没法再把它取出来
8.22 Getting方法
8.23 Setting方法
Setting方法是根据实现来命名的,而不是根据意图
最好能理解客户设置这个值是为了解决什么问题,直接提供一个解决问题的方法
把setting方法作为接口的一部分泄露了实现
paragraph.setJustification(Pragraph.CENTERED);
根据方法的意图来命名有助于代码的表达
paragraph.centered();
哪怕centered()的实现其实是一个setting方法
Paragraph.centered() {
setJustification(CENTERED);
}
8.24 安全副本
8.25 小结
第9章 容器
9.1 隐喻
容器融合了不同的隐喻。第一种是多值变量(multi-valued variable)。一个变量引用一个容器,感觉就像它同时引用了多个对象。这样来看,容器就不再是独立的对象了。容器本身不再受到关注。人们关心的只是它所引用的多个对象。就像所有的变量一样,可以把它分配给一个多值变量(通过添加和移除元素),获取它的值,向它发送消息(在for循环中发送)
多值变量这个隐喻在Java中失灵了,因为java中的容器就是独立的对象实体。
9.2 要点
容器的第一个概念就是它的大小
容器的第二个概念是元素是否有序
另外一点就是元素的唯一性
最后一点,容器的选择也会影响到性能
9.3 接口
9.3.1 Array
9.3.2 Iterable
9.3.3 Collection
9.3.4 List
9.3.5 Set
9.3.6 SortedSet
9.3.7 Map
9.4 实现
9.4.1 Collection
9.4.2 List
9.4.3 Set
9.4.4 Map
9.5 Collections
9.5.1 查询
9.5.2 排序
9.5.3 不可修改的容器
9.5.4 单元素容器
9.5.5 空容器
9.6 继承容器
9.7 小结
第10章 改进框架
10.1 修改框架而不修改应用
10.2 不兼容的更新
10.3 鼓励可兼容的变化
10.3.1 程序库类
10.3.2 对象
10.4 小结
附录A 性能度量
参考书目