继承是C++面向对象设计的灵魂。继承+虚函数是使用最多的多态形式,继承可给程序员带来编码清晰,结构简单,易于实现等诸多方面的好处。但是不是继承使用越多就越好呢?
实践证明,过广或过深的继承往往不是良好设计的表现。这种设计会给我们带来许多不良问题,这种继承系谱的出现主要缺陷是类型职责划分不清楚。
图11-6是一个关于形状的继承系谱。这个系谱图展示了Circle,Square,Triangle三种形状与Shape基类的继承关系。体现各自的职责范围,是一个很好的设计。
作为绘图的Shape,存在颜色属性是自然不过的了。但是如果要求你设计一个形状包括红色或蓝色的Shape。那你应该如何设计呢?图11-7是一个入行不久的初级程序员设计UML图。
如图11-7所示的继承系谱,存在着严重的问题。每向其中添加一种颜色,我们就必须要为所有的形状添加一个新的派生类。同样每添加一个新的形状,我们也需为其添加所有的颜色相关的派生类类型。这就是典型的“指数级”膨胀的继承谱系。这种实现和做法是很愚蠢的。我们可采用复合设计模式替换“指数级”膨胀的继承谱系,如图11-8所示。
避免设计“指数级”膨胀的继承,应该从职责划分的角度,寻找更好的解决方案。在某些情况下,采用复合设计模式替换这种“指数级”膨胀的继承是一个不错的选择。
如图11-8所示的设计,CShape类型复合了一个Color类型,这种可以保证任何同CShape继承的形状都复合这种类型,避免的继承类的指数膨胀,因此是一个不错的选择。但是有时并非所有实现都这么显而易见。有的往往隐藏很深,甚至很难发现。我们看某同学设计的手机模拟系统,如图11-9所示。这种设计符合人的直观思维,但是对于软件设计而言,可是一个灾难。
如果客户要求添加一种手机模拟。要求这种手机支持通讯录和游戏,同时还支持通话模拟。我想这时候你会傻眼的,你必须为通讯录添加一个派生子类,为游戏添加一个派生子类,还需要为手机软件添加一个直接通话模拟子类,然后在添加一个本品牌手机模拟通话子类。这听起来就是一个很烂的设计。
思维陷阱
- 对象的继承关系在编译的时候进行绑定,所以无法在运行的时候改变从父类继承的实现。子类的实现与它的父类有着非常密切的关系,以至于父类的任何变化都会影响大子类的实现。
- 当你需要复用子类是,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系最终限制了灵活性和复用性。
为何不将手机品牌和手机软件的耦合降低呢?手机品牌就是手机品牌和手机软件其实没多大关系,手机仅是软件的载体,同样的软件可在不同的手机品牌上运行。这是自然不过的道理了。我们看一个手机品牌和手机软件松耦合的设计方案,如图11-10。这种方案就是经典的“桥接模式”。
// 手机软件基类
class IHandSetSoft
{
public:
virtual ~IHandSetSoft(){ }
virtual void Run() = 0;
};
// 手机游戏
class CHandSetGame :public IHandSetSoft
{
public:
virtual void Run()
{
cout << "运行手机游戏" << endl;
}
};
// 手机通讯录
class CHandSetAddrList :public IHandSetSoft
{
public:
virtual void Run()
{
cout << "运行手机通讯录" << endl;
}
};
// 手机品牌基类
class IHandSetBrand
{
public:
virtual ~IHandSetBrand()
{
m_pHandSetSoft = NULL;
}
void SetHandSetSoft(IHandSetSoft *pHandSetSoft)
{
m_pHandSetSoft = pHandSetSoft;
}
virtual void Run() = 0;
private:
IHandSetSoft *m_pHandSetSoft;
};
// 手机品牌M
class CHandSetM :public IHandSetBrand
{
public:
virtual void Run()
{
cout << "手机品牌M" << endl;
m_pHandSetSoft->Run();
}
};
// 手机品牌N
class CHandSetAddrList :public IHandSetBrand
{
public:
virtual void Run()
{
cout << "手机品牌N" << endl;
cout << "运行手机通讯录" << endl;
}
};
桥接模式将抽象部分和实现部分进行分离,使其分别可以独立变化。桥接模式中抽象和实现分离指将抽象和实现的紧耦合转换为关联关系的弱耦合。
桥接模式的核心意图是把实现独立出来,让他们各自地变化。这使得每种实现的变化不会影响其他实现,从而达到应对变化的目的。
请谨记
- 过广或过深的继承往往不是良好设计的表现。类型职责划分不清楚是造成这种继承系谱主要原因。对继承滥用保持警惕,同时警惕常理逻辑在软件设计中的负面影响。
- 对于“指数级”继承膨胀问题,需寻找合适的解决方案,以防类似问题继续蔓延。