谈到面向对象编程中如何选择对象之间的关系,实在是一件令人头疼的事。
对象之间的关系大致有以下几种:
1. 继承关系
举个例子:Person是一个描述所有人的类,Student可以继承于Person,所有Person具有的公有方法Student都有,即任何使用Person对象的地方,都可以毫无顾忌的直接用Student代替。
这种关系是非常强的,有时候产生的问题会让你始料未及,而且很难修改和重构,特别是如果想修改基类的时候,可能就会导致不是所有的子类都能适用了。例如:
Bird类是描述所有的鸟的类,有一个Penguin类继承自Bird类;最初的时候Bird类没有fly()这个公有接口,所以Penguin类可以正常使用,现在想要让fly作为Bird类的公有接口,那么所有Bird的子类都必须实现这个新的接口,但是问题来了,Penguin是不能飞的,如果强行用抛出异常来实现这个接口就必须只能在运行期才能检查出来了。这其实就导致了“不好的接口”。时间一长,程序的维护将会非常困难。
继承关系用C++表示出来如下:
class Base
{}
class Derived : public Base
{}
2. 聚合关系
传统的OO书上将组合关系还细分为组合和聚合。它们的区别可以从对象使用者的角度来看,如果是组合,那么使用者不需要知道对象内部组合的东西;如果是聚合,那么使用者需要知道对象组合了什么。
这么看来,聚合通常是比组合更松的组合,通常类似于一个容器的概念。组合则类似于多个小的组成部分(可以是相同的类,也可以是不同的类)共同组合成了一个大的对象,没有容器的概念。
所以,聚合我们就把它当作容器来看,如果聚合对象有一些特有的公有方法的话,可以用一个模板容器类来做聚合类(例一:一般的Pool,有一些特有的方法,可以包装一下stl的list/deque等容器,提供特定的方法接口出来);如果简单的话,直接用stl提供的集合即可。
组合关系其实也是很强的关系,而且针对不同的应用场景,划分组合的方法也不同。例如,对于人来说,可以说是由头,躯干,四肢组成,任何部位不能单独被外界直接接触,而必须通过人这个组合对象来间接操作,甚至外界根本不知道人的组成结构,只知道人这个东西可以走路,思考等等。组合关系隐含了很强的封装思想,但是带来的可能是不灵活性,万一以后的新需求需要让外界知道组合体内部的情况则显得会有点乱;而且组合体内部对象的构造也可能是个问题,因为组合体内部对象的构造可能不是组合体自己就能完成的,可能需要借助外部,这样组合体可能需要依赖于一系列工厂,或者利用现代面向对象更多会采用的依赖注入。
组合关系用C++表示出来如下:
class Comb
{
public:
(public methods...)
private:
ClassA a;
}
聚合关系类似于stl的容器。
3. 使用关系
继承和聚合关系中两个对象的关系都比较强,而使用关系是一种表达两个对象弱相关性的关系,包含传统的UML中的关联,依赖这两个关系。
使用关系描述的是一个对象可以使用另一个对象完成某件自己想完成的事。
使用关系也可以叫做委托关系,就是把一项任务委托给另一个对象去完成的含义。这样把任务细分,就类似于人类社会的分工一样,分工越细,社会也越进步。
使用关系用C++表示出来如下:
class ClassA
{
private:
ClassB *b; // 用指针因为ClassA只是使用了b,b必须从外面设置进来
}
这个实现和UML中的关联关系是一致的。
在使用关系的使用中,最好把被使用对象(上面的ClassB)设置为抽象基类,而且要从ClassA的角度来为ClassB命名,这样ClassA的实现可以很单纯,不需要有不必要的依赖。
如果ClassB中只有一个公有接口,那么可以像C#里面使用委托对象来代替(其实就是一个std::function),无需再为此专门弄一个类了。
另外,如果把ClassB *b放到方法调用的参数中,那么就是UML中的依赖关系了。
小结:
1. 继承关系是最强的一种关系,如果基类接口以后会更改,那么千万不要采用继承关系。
2. 聚合关系是描述系统中的实体的主要方法,大多数情况下可以使用树形结构来描述系统中的实体的关系,采用聚合关系是非常合适来构造树形结构的。
3. 使用关系是最常用的,在两个弱相关的对象之间,使用关系是唯一的选择。使用关系的建立往往需要其他对象来完成,使用第三个对象把两个对象的关联关系建立起来。