Class
类是对一组相似的东西的一般归纳,而对象则是这些东西本身。这里介绍的是与类相关的一些模式。
类(class)
一个类是一个这样的声明:这些逻辑应该放在一起,它们的变化不像它们所操作的数据那么频繁;这些数据也应该放在一起,它们变化的频率差不多,并且由与之关联的逻辑来负责处理。
当然也并不全都是数据变化,逻辑不变;有时候数据的变化会影响到逻辑的变化,就像是国产产品和进口产品对应的税金计算方式不同;有时候数据在计算过程中不会发生变化。需要我们学习如何用类来包装逻辑和如何表达逻辑的变化。
类的继承体系也可以缩减代码量,但是它让代码变得更难懂,必须理解超类上下文。子类是超类的继承,代表我和超类很像,但是有些许的不同。这个在命名上要多加注意。
在OOP中,类是相对昂贵的设计元素,一个类应该做一些直接和明显的事,不要泛滥。
简单的超类名(Simple Superclass Name)
位于继承根上的类应该有简单的名字,用以描绘它的隐喻。一个好的命名有助于对整个程序继承体系的了解。Kent建议重要的类,尽量用一个单词来为它命名。
限定性的子类名(Qualified Subclass Name)
子类的名字应该表达出它与超类之间的相似性和差异性。所以子类的名字有两重责任:1 描述这些类像什么 2 说明它们之间的区别是什么。
我们在使用中声明一般使用的是超类,定义时才会用到子类。所以子类名称在交谈中使用的不频繁,这里不需要太过考虑子类名称的简单。我们可以在超类名称的基础上增加1、2个单词来得到我们的子类名称。
当然实际操作中不会有这么简单的处理方式,并不是所有的子类名都适合在超类名上扩展。就好比:超类名为Language,那么子类完全可以叫做Chinese,而不是Chinese Language。尤其是对于多重继承来说,在超类名的基础上扩展绝对是一场灾难。
要根据实际情况,灵活处理,谨记子类名的命名原则:表达相似性和差异性。
抽象接口(Abstract Interface)
将接口与实现分离。意味着决策不应该暴露给不必要的地方。抽象借口在Java中意味着interface和超类,其中超类是抽象的。
一般认为接口就意味着灵活。但这里谈论灵活的时候我们都忽略了2个东西。
1、 灵活是需要成本的。出于成本的考虑,我们应该在真正需要这种灵活性的时候才引入接口
2、 软件存在不确定性。我们无法预料软件变化的方向。由于面向对象的特性,新增对象的成本远低于新增方法的成本,所以不必要的灵活处理,可能在后期给我们造成极大的修改难度。
所以,我们应该在确定无疑地需要灵活时,才应该引入这种灵活性。谨记:灵活是有成本的。
Interface
Java中Interface表达“这是我要完成的任务,除此之外的细节不归我关心”。
对interface增加或是修改方法,就必须改变它的所有实现类,这是interface的一个使用上的风险。另外interface要求所有的方法必须为public的,这比较容易暴露我们的设计思想,也是一个风险。
对interface的命名也存在2中方式,取决于我们如何看待它们。一种是把它当作“没有实现的类”;另一种就是从命名中声明为“抽象的类”。
有版本的Interface(Versioned Interface)
如果我们一定要修改一个接口,但又无法修改它的所有实现类怎么办?这里我们可以考虑使用有版本的interface。声明一个新的interface,使它继承原来的interface,然后再其中增加操作。如果使用者需要新增的功能,就用这个interface,否则的话就继续无视新的interface。但使用新的interface时有可能需要instanceof来判别。
抽象类(Abstract Class)
抽象接口的另一个实现方式是使用超类。超类是抽象的,因为超类的引用可以在运行时替换为任何子类的对象;至于这个超类在Java的语法意义上是不是抽象的,这并不重要。
那么我们什么时候使用超类,什么时候使用interface呢?这个取决于①接口会如何发生变化,实现类是否需要同时支持多个接口;②抽象借口需要支持实现的变化以及接口本身的变化两种类型的变化。
Interface对第二点的支持不佳,一旦接口变化就需要实现类进行修改,多了以后就容易致使程序瘫痪,或是借助有版本的interface
抽象类没有这些限制。只要提供了默认实现就不会影响到实现类。但是一个实现类只能继承一个抽象类(实现类对抽象类必须忠贞不二,^_^),限制了它的使用。
值对象(Value Object)
这种对象的行为就好像数值一样。就好像是Integer似地,虽然是类,但我们一般把它当作是int来使用的。
值对象的所有状态都应该在构造子中传入,其他地方不再提供改变其内部状态的方式。值对象的操作总是返回新的对象,操作的发起者要自己保存返回的对象。
值对象这种风格适用于其中状态不会发生变化的情况。
特化(Specialization)
清晰的描述计算过程中相似性与差异性的相互作用,可以让程序更容易阅读、使用和修改。
实际代码中经常出现这样的情况:数据可能大多相同,但略有些区别;逻辑可能大多相同,但略有些区别。
特化我觉得在这里可以用《设计模式》中提到的面向对象的一个特性来解释,就是类、方法的职能要单一。如果职能不单一,那么我们就很难区分功能,也就无从判别其中的相似性和差异性了。
子类(SubClass)
声明一个子类就是说,我与超类很相像,就是在XXX上有点不同。是面向对象很有用的一个手段。
当然,继承也不是万灵丹,也是有着诸多的限制。首先,写子类前必须先了解超类的结构和功能;其次,后期如果要修改超类,可能会对它的子类造成灾难性的破坏;再次,子类继承于超类,可以利用超类功能,但是同时也是对子类的一种约束,限制了子类的功能范围。
另外超类的方法功能是否单一,也会影响到子类的结构。如果超类的每一个方法都具有单一性,那么实现子类时,只需要覆写特定的几个或一个方法,也更加容易表达出子类的意图。否则的话,覆写超类方法时,还需要拷贝一堆无用代码过来,可真就无趣的很,也背离了我们特化(Specialization)的要求。
实现器(Implementor)
覆盖一个方法,从而表现一种计算上的变化。 Kent在文中使用了“意图”和“实现”两个词。“意图”暴露给客户,告诉他们我们要做什么;“实现”隐藏起来,是由开发者完成的具体实现。
为了在一个类内达到“意图”和“实现”分离,Kent提出了另一个面向对象的特性——多态。
多态,保证了同一个“意图”可以有多个不同的“实现”。这里用“特化”声明了相似性。
内部类(Inner Class)
我们可以内部类相当于将局部有用的代码放在一个私有的类中。当我们需要把某一部分计算逻辑包装起来,但是又不想新建一个文件来安置全新的类,这时就可以声明一个小的私有类(内部类),这样就可以低成本的获得类的大部分好处。
内部类有一个特点:当内部类被实例化时,它的对象会悄悄地获得创建它的那个对象。如果,我们想将内部类和所处的对象分离,我们可以将其声明为static的。
实例特有的行为(Instance-specific Behavior)
理论上来讲,每个类的所有实例逻辑都应该是相同的。但在实际中每个实例的逻辑都有不同。这会导致代码在阅读时不易理解,最好在实例创建的时候就确定所有的行为,并不再变化。
条件语句(Conditional)
要实现实例特有的行为,if/then和switch语句是最简单的方式。但是条件语句需要重复的话,造成的影响就会很大,不如把条件逻辑编程消息,发送给子类或委派。这里可以参照Martin Fowler《重构》中关于影片租赁系统的代码的重构,很精彩。
委派(Delegation)
把部分工作委派给不同类型的对象,不变的逻辑放在发起委派的类中,变化的逻辑交给被委派的对象。
这里我们可以考虑把委派的对象作为参数传递给接受委派的方法。这里可以参见设计模式中的策略模型。
可插拔的选择器(Pluggable Selector)
通过反射来调用方法,以表现不同的逻辑。
使用可插拔选择器的代价很大,比如你可能删除一段看起来从未被调用的代码而导致系统崩溃,因为这段代码是别人通过发射调用的。所以需要解决某些特别困难的问题时才值得用这样的代价来换取,因此应该严格限制对这种技巧的使用。
匿名内部类(Anonymous Inner Class)
这种方法可以创建“只在一处使用的类”,这些类可以覆盖一个或者多个方法,完全为了当前的用途。由于只在一处使用,这样的类可以被隐式的引用,不需要有名字。
匿名内部类有个很麻烦的地方,就是在TDD中很难直接测试到。
类库(Library Class)
如果一组功能不适合放进任何对象,就将其描述为一组静态的方法。就像是Collections,Arrays等就是类库。