2.“开— 闭”原则的实现
“开—闭”原则为设计提供了目标,但却没有明确给出实现的手段,下面说明在面向对象的设计中实现“开—闭”原则的方法。
(1)面向接口的编程
面向接口的编程的优势如下:
a.降低程序各部分之间的耦合性,使程序模块互换成为可能。这样客户无需知道自己使用的对象的类型,只要对象有客户所期望的接口即可。并且客户也不需要知 道对象是如何实现的,只要知道定义接口的抽象类。
b.使软件各部分便于单元测试,通过编制与接口一致的模拟类(Mock),可以很容易地实现软件 各部分的单元测试。从而提高软件的可靠性,降低错误率。
c.已于实现软件的模块的呼唤,软件升级时可以只部署发生变化的部分,而不会影响其它部分。
对于设计模式来说,创建型模式的产生是面向接口编程的 必然结果,面向接口编程要求使用对象的客户不了解对象的具体类型,客户只能通过一个负责实例化对象的对象——我们称之为“工厂”——来实例化对象,而工厂 对象在应用初始化时集中进行实例化或者在集中的模块中进行。
3.封装变化
程序中任何可能发生变化的部分都可以封装为对象,包括命令、事件、属性、算法和状态等。封装变化是实现“开—闭”原则的重要手段,也是在设计中发现对象的 重要途径。因此在分析需求时,一定要注意什么是不变的,什么是可能发生变化的,以及这些可能的变化会对封装带来的影响。
例如,如果对象的行为基本 不变,那么这些行为可以作为对象的方法;否则就要考虑是否抽象和封装这些行为。再如,状态可以用状态参数来表示,例如温度和压力等。可以将状态参数作为独 立的属性,但如果状态参数之间相互关联,则有必要进行抽象。
总之,封装可能发生变化的部分,将可能的变化作为对象。在面向对象设计中,对象不仅是指现实中存在的事物或者可视的事物,任何可能变化的部分都是侯选对 象。
4.采用组合替代继承
继承是面向对象系统的特点之一,没有继承,很多面向对象的设计就无从谈起。在设计模式中,没有哪个模式不涉及到继承。采用组合替代继承并不适合任何情况, 实际上,如果没有继承,很多组合根本无法实现,涉及到基础结构的继承无法替换。
组合和继承针对模块中的复用而言,当功能需要扩展时,采用继承实现复用比较简单直观。只要派生一个类,在这个类中增加新的特性,即可实现对现有类复用。然 而类继承在编译时定义,无法在运行时改变。并且继承对子类暴露了父类的实现细节,从而破坏了封装性,使子类与父类耦合性非常强。一旦父类发生变化,必然导 致子类也发生变化。如果继承下来的实现不能解决新问题,则需要修改父类,这种依赖性限制了灵活性。
如果我们希望通过增加子类来扩展功能,可能会出 现如图所示的情况。
如果不修改类而仅仅依靠增加子类扩展,尽管似乎满足“开—闭”原则,但结果变得非常可笑。替换这个方案的一种方法是采用对象组合,将人的行为抽象为类,如 图所示。
通过与行为对象的组合,可以扩展“人”的行为。通过增加新的行为类来实现扩展,类层次并没有增加。
对象组合在运行时通过获得对其他对象的引用来实现,组合要求对象遵守彼此的接口约定。只要符合这个约定,一个对象可以在运行时动态地玮不同的对象组合,从 而实现复用。在设计时,每一部分只要关注接口约定即可,不必考虑具体的实现。因此对象组合有最小的耦合性并且更灵活,在设计模式中大量地采用了对象组合。
再次强调,继承是面向对象的基石之一。采用组合代替继承者是在需要复用的前提下,并不是所有的继承都可以用组合替代。不仅如此,没有继承,组合也无从谈 起。在上面的例子中,行为的扩展仍然需要增加行为子类。