问题
用面向对象的方法建模描述一个小型的动物世界。这个缩微的动物世界中,有下列成员:老鹰,会飞翔的肉食动物;狮子,行走的肉食动物;麻雀,会飞的草食动物;牛,会走的草食动物。
要求
建立可以统一描述所有动物的模型,使得用户使用该模型时可以忽略动物的具体类型。
方案一
飞翔、行走、吃草、吃肉都是动物的能力或者行为,而行为可以用接口来描述。于是非常straighttforward地闪现出一个方案:
定义了四个接口,walk,fly,meat和grass,分别代表四种能力,具体的动物实现相应的接口,以具备相应的能力。不幸的是,该模型基本没什么用处,它没有构成一棵继承树,对外没有表现出统一的接口,无法发挥多态的威力。
建模不成功的根本原因在于:对问题域分析得不深入,概念还没理清,抽象得还不够。进一步分析可知,飞翔和行走都是动物的移动方式,吃草和吃肉都是进食方式。比如牛的移动方式是行走,进食方式是吃草;老鹰的移动方式是飞翔,进食方式是吃肉。不过要注意,有的动物在某些特性上不是单一的,比如鸟类基本上既可以行走又可以飞翔;杂食性动物既吃草又吃肉。
方案二
于是可以提取出动物这个抽象概念,其具有move和eat两种行为。按此思路有如下方案二:
方案二是典型的继承复用思路。严格按照分类学意义进行建模,按照不同特性的组合区分出子类。存在两个问题:
1、组合爆炸。在上例中有两个特性可能变化,每种特性各有两种可能,可以派生出2*2个子类。试想,如果有三个变化的特性,每种特性有三中可能,那么就可以派生出3*3*3个子类。系统中的派生类数量将呈几何级数增长,这就是继承中的组合爆炸问题。
2、代码重复。在草食行走动物中,实现了eat_grass()和walk()两种方法;而草食飞行动物中,也要实现eat_grass()方法。重复的代码是bug的来源之一。
从上可知,当同一层次的不同子类间只有一个特性差异时,用继承能够很好的描述其结构,且不会产生组合爆炸问题。当特性差异超过一个时,就会产生组合爆炸问题。当然如果可以确定派生类的数目有限且将来不会发生变化,上述继承方案也是可行且在实践中被广泛使用的。
方案三
如何改进?用聚合代替继承。于是有了方案三:
该方案是桥梁(Bridge)模式的产物。OO设计的基本方法是“对可能发生变化的地方进行抽象”(原文是“对可能发生变化的地方进行封装”,由于抽象也是封装的一种手段,我将其改成这样)。方案二中将可能发生变化的move和eat行为用虚拟方法进行抽象,从而导致的必然是使用“继承”思路的设计。方案三中对move和eat行为用接口进行抽象,并将接口聚合到基类抽象类中,从而带来了很多的优点。
1、避免了组合爆炸。添加新的特性,只要增加新特性的接口和具体派生类,类数量的增长是线性的。
2、避免了代码重复。新特性完全由新添加的策略类实现,不会散落在各个主逻辑的类中。要使用新特性,只需要聚合新类型的接口即可。
3、可在运行时动态改变对象行为。比如,老鹰大部分时间在飞翔,但有时候还是需要行走。在继承方案的设计中,老鹰已经具备了飞行能力,若要使其具有行走的能力,必须修改代码,让其从具备行走能力的基类中继承,然后重新编译,但副作用是老鹰又失去了飞行的能力;如果要使老鹰同时具有飞行和行走的能力,在现有系统中尚无对应的实现,只能再编写一个类似fly_and_walk(int
moveMode)的方法,让老鹰根据不同的条件飞行或者行走。然而,在基于桥梁模式的设计中,老鹰持有move接口,当需要飞行时,它将move接口实例化为fly具体子类,然后执行move.move();当需要行走时,它将move接口实例化为walk具体子类,同样执行move.move()。不用重新编译,也没有增加move抽象方法的新实现。当增加新的移动方法,比如swim时,桥梁模式方案也能轻松应对,不用修改,所需做的只是在需要的时候,执行move
= new swim()。
为什么采用桥梁模式呢?桥梁模式的用意是"将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化"。仔细分析,动物本身分类的演化和其移动方式(或者进食方式)的演化本就属于不同继承结构,将二者关联起来的方法就是将一个聚合到一个中,自然就构成了桥梁模式。
从继承到聚合的思路转换线索
1、需建立的派生类过多,特别是要添加对新变化的支持使得派生类数量迅速膨胀时,可以考虑从继承转换到聚合。
2、派生类可能在某些情况下转变成另外一个类的派生类时,需要检查继承结构是否合理。比如老鹰从肉食性飞行动物继承,但当它要走路时,需要变成肉食性行走动物的派生类,这说明老鹰作为肉食性飞行动物或者肉食性行走动物的派生类都是不合理的。因为继承关系应该满足分类学意义,基类和派生类的关系应该是明确而稳定的。
结论
1、慎用继承。仅当满足里氏代换原则的时候才使用继承。
2、聚合比继承更加灵活。
也许可以这样讲(不一定对),用继承构建核心概念体系结构,用聚合实现核心概念的各种行为,特别是需要运行时变化的行为。
|