面向对象编程——继承与多态
业务领域中找出软件对象,而软件对象与现实世界中对象的行为完全不一样。软件对象以点对点的通信方式通过发送消息进行交互,而现实世界中的对象与环境的交互,以及其它对象动态地反映现实世界的对象之间交互都要丰富得多。
经验丰富的开发人员在研究领域时,如果发现了他们所熟悉的某种职责或某个关系网,他们会想起以前这个问题是如何解决的。以前尝试过哪些模型?在实现中有哪些难题?它们是如何解决的?先前经历过的尝试和失败的教训,会突然间与新的情况联系起来。
为了真实地反映现实世界中对象的动态交互,要让一个类在不同的系统中重用,则必须在设计类时充分考虑扩展性。经过长期的积累,人们总结了一套用于启发和指导类的设计原则:职责驱动设计——如何为协作中的对象分配职责。
显然,对于rangeValidator对象和oddEvenValidator对象来说,它们的职责分别是对push到栈中的数据进行范围值校验和偶校验,也就意味着必须存在相应的方法。由于每个子类都要对自己的行为负责,因此每个子类不仅要提供一个名为validate的方法,而且必须提供它自己的实现代码。比如,RangeValidator和OddEvenValidator都有一个validate的方法,RangeValidator类包含范围值校验的代码,OddEvenValidator类肯定有奇偶校验的代码。它们都是Validator的子类,必须实现其不同版本的validate。
不言而喻,OOP比POP更直接地表达了校验器的共性:"使用validate函数指针在运行中根据对象的类型调用不同的函数,并通过pThis指针指向当前对象引用校验参数将共同的部分打包在一起形成抽象类。"当它们有了这种共性时,则更容易讨论各种校验器相互之间的差别。
除了变量value之外,RangeValidator类对象的validateRange()校验函数的共性是符合范围值条件的判断处理语句,其可变的是范围值校验参数min和max;OddEvenValidator类对象的validateOddEven()校验函数的共性是符合偶数值条件的判断处理语句,其可变的是偶校验参数isEven。
根据共性和可变性分析原理,将稳定不变的相同的处理部分都包含在抽象的模块中,可变性分析所发现的变化的变量由外部传递进来的参数应对。其函数原型如下:
由于&rangeValidator.isa、&oddEvenValidator.isa和pThis值相等,且类型也相同,因此可以将范围值校验和奇偶校验函数的void *泛化为Validator *。当将一个基类对象替换成它的子类对象时,程序将不会产生任何错误和异常,且使用者不必知道任何差异,反过来则不成立。也就是说,如果某段代码使用了基类中的方法,必须能使用派生类的对象,且自己不必进行任何修改。因此在程序中要尽量使用基类类型定义对象,在运行时再确定其子类类型,用子类对象替换基类对象。这就是里氏替换原则,它是由2008年图灵奖获得者,美国第一位计算机科学女博士Barbara Liskov教授和卡耐基梅隆大学Jeannette Wing教授于1994年提出的。
在应用里氏替换原则时,应该将父类设计为抽象类或接口,让子类继承父类或实现父类接口,并实现在父类中声明的方法。在运行时子类实例替换父类实例,可以很方便地扩展系统的功能。无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类实现,由此可见,里氏替换原则是实现开闭原则的重要方式之一。
如果开闭原则是面向对象设计的目标,那么依赖倒置原则就是面向对象设计的主要原则之一,它是抽象化的具体实现。依赖倒置原则要求传递传递参数时或在关联关系中,尽量引用高层次的抽象层类,即使用接口和抽象类进行变量的声明、参数类型的声明、方法返回类型的声明,以及数据类型的转换等,而不要用具体类做这些事。
为了确保该原则的应用,一个具体类应该只实现接口或抽象类中声明过的方法,而不是给出多余的方法,否则将无法调用在子类中增加新的方法。显而易见,在引入抽象层后,将具体类写在配置文件中。如果需求发生改变,则只需要扩展抽象层,修改相应的配置文件即可。而无须修改原有系统的代码,就能扩展系统的功能,满足开闭原则。通常开闭原则、里氏替换原则和依赖倒置原则会同时出现,开闭原则是目标,里氏替换原则是基础,依赖倒置原则是手段,它们相辅相成相互补充,其目标是一致的,只是分析问题的角度不同。
继承是OO建模和编程中被广泛滥用的概念之一,如果违反了LisKov替换原则,继承层次可能仍然可以提供代码的可重用性,但是将会失去可扩展性。因此在使用继承时,要想一想派生类是否可以替换基类。如果不能,则要问一问自己为何使用继承?如