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