实现模式:类
数据的变化比逻辑要频繁的多,正是这种现象让类有了存在的意义。每个类其实就是这样一个声明:这些逻辑应该放在一起,它们的变化不像他们所操作的数据那么频繁;这些数据也应该放在一起,它们的 变化频率差不多,并且由与之相关的逻辑来负责处理。
这种“数据会变、逻辑不变”的划分并非绝对适用:有时随着数据值的不同,逻辑也会有所不同;有时逻辑也会发生相当大的变化;有时数据本身在计算的过程中反倒不会改变。学会如何用类来包装逻辑和如何表达逻辑的变化,这是有效使用对象编程的重要部分。
把多个类放进一个继承体系可以缩减代码量,比不原封不动的把超类的内容抄到子类精简的多。和所有缩减代码量的技巧一样,它也让代码变的更难读懂;必须理解超类的上下文,然后才有可能理解子类。
在由对象搭建而成的程序中,类是相对昂贵的设计元素。一个类应该做一些直接而明显的意义的事情。减少类的数量是对系统的改进,只要剩下的类不因此而变的臃肿就好。
实现模式:简单的超类名
照到一个贴切的名字是编程中最令人开心的时刻之一。在所有的命名当中,类的命名是最重要的。类是维系其他概念的核心。一旦类有了名字,其中操作的名字也就顺利成章了。
实现模式:限定性的子类名
子类的名字有两重职责,不仅要描述这些类像什么,还要说明他们之间的区别是什么。同样,在这里需要权衡长度和表现力。与位于继承体系根上的超类不同,子类的名字在交谈中用的并不频繁,所以值得以牺牲简明来换取更好的表现力。通常在超类名的基础上扩展一两个词就可以得到子类名。
与他人沟通时类名的用途,如果仅仅为了和计算机沟通,只要给每个类编号就足够了。太长的类名读写都费劲,太短的类名又会考验读者的记忆力。如果一组类的名字体现不出他们之间的相关性,阅读者就很难对它们形成整体印象,也很难回忆起它们的关系。应该用类名来讲述代码的故事。
实现模式:抽象接口
请牢记软件开发的古训:针对接口编程,不要针对实现编程。从另一个角度来说,这也意味着设计决策不应该暴露给不必要的地方。如果大部分代码只知道我在处理一个容器,那么我就可以随时改变这个容器的具体实现。但有时不得不指定具体类,否则计算就没法进行下去。
这里所说的“接口”是指“一组没有实现的操作”。接口这个概念既可以表现为interface,也可以表现为超类。随后的两个模式会分别指出两者的适用场景。
每层接口都是成本。并不是接口数量越多软件成本就越少,只有需要接口带来的灵活性时才值得为它付出成本。
实现模式:interface
要用java表达“这是我要完成的任务,除此之外的细节不归我操心”,可以声明一个interface。Interface是一个很好的平衡,它带来了多继承的一部分灵活性,同时又没有多继承的复杂性和二义性。
如果说interface让改变的工作更加轻松,那么不能不提的是对接口本身的修改时不被鼓励的;一旦在interface上增加或修改方法,就必须同时改变所有的实现接口的类。如果无权改变实现,大量使用接口会严重拖累日后的设计调整。
此外interface的一个特点也影响了他们作为沟通手段的价值:其中所有的操作都必须是public的。
实现模式:抽象类
在java中区分抽象接口和具体实现的另一种方式是使用超类。超类是抽象的,因为超类的引用可以再运行时替换为任何子类的对象;至于这个超类在java中的语法意义上是不是抽象的,这并不重要。
是应该使用超类还是应该使用interface?取舍最终归结为两点:接口会如何变化,实现类是否需要同时支持多个接口。
抽象接口需要支持实现的变化以及接口本身的变化两种类型的变化。Interface对接口本身的变化支持不佳;一旦改变interface,所有的实现类都必须同时修改。抽象类则没有这方面的限制。只要提供了默认实现,在抽象类中新增的操作就不会侵扰现有的实现类。
抽象类的局限体现在实现类必须对其忠心不贰。如果需要以另一种视角来看待同一个实现类,就只能让它实现interface了。
实现模式:有版本的interface
如果想要修改一个interface但又不能修改,怎么办?这种情况通常在想要增加操作是发生,在interface中增加操作会破坏所有现有的实现类,所以不能这样做。不过可以声明一个新的interface,使它继承原来的interface,然后在其中增加操作。如果使用者需要新增的功能,就使用这个新的interface,其他的使用者则继续无视新interface的存在。使用这种做法,在需要新功能是必须明确检查对象的类型,并将其向下转型为新interface的类型。
interface能很好的适应实现的变化,却不容易适用自身结构的变化。