StupidBeauty
Read times:3153Posted at:Mon May 29 11:26:01 2017 - no title specified

《C++面向对象软件设计及构建》文章翻译:6.10多重继承,6.10 Multiple Inheritance

多重继承,指的是,派生类同时继承了多个基类。之前的示例中,都只是用到了单重继承,也就是,每个派生类都只继承一个基类。多重继承,有个明显的好处,那就是,它使得多个基类的行为能够直接且无缝地组合起来。不过,并非所有的面向对象语言都支持多重继承。反对多重继承的人,经常拿使用过程中的一些微妙问题来说事,例如,当一个派生类同时从多个基类继承了方法相同(也就是说,方法的特征签名一致)而代码不同的方法时,其行为会是什么样的。

在本章节中,会说明两种对于多重继承来说狠有用且直观的场景:将两个正交的(orthogonal)(互相独立的)基类组合起来;以及,将一个抽象接口和一个实体实现组合起来。前面一种用法中,通过将已有的基类组合起来而提升了代码复用性。而后面一种用法呢,增加了额外的灵活性,并提升了多态性。

1 将正交的基类组合起来

如果两个基类之间没有任何重叠的方法或数据,那么,我们就说它们互相之间是独立的或正交的。此处所说的正交,指的是,这两个类是在两个不同的维度上工作,并不会在互相之间以任何方式产生干扰。符合这种情况的基类,可以轻易地被同一个派生类继承。

实际情况中,对于某些类,可能会发现这样一点,或者故意制造出这样的情况,使得,它们所代表的属性,就是互相之间正交的,或者近似于正交的。这种类,就可以轻易地与其它类一起被继承,以便将它们的属性组合起来。有些时候,会使用“混入类”("mixin classes")这个术语来描述这些类,因为,我们可以方便地将它们的属性与其它类的属性混合到一起。下面展示的Named 类,即是混入类的一个示例,它表示的是,一个实体拥有一个字符串表示的“名字”("name")属性。一个命名(Named)对象, 在其构造过程中,必须提供一个名字,它的名字可查询(GetName)、可修改(SetName)、可与其它名字比较(IsNamed)


利用多重继承来创建一个命名数字( NamedNumber

class NamedNumber : public Number, public Named

{

  public:

    NamedNumber(int initialValue, char* name);

   ~NamedNumber();

};

// 实现

NamedNumber::NamedNumber(int intialValue, char* name)

 : Number(initialValue), Named(name) {}

对于那些需要拥有一个名字的对象,就可以通过继承Named 类的方式来获得这个属性,至于其它的基类, 它可以照常继承。例如,要想创建一个NamedNumber(这种实体,拥有Number(数字)的所有能力,并且,还拥有一个以字符串表示的名字)的话,就可以轻易地按照如下所示的多重继承来实现。


一个简单的混入类

class Named

{

  private:

    char* name;

  public:

          Named(char* originalName);

    int   IsNamed(char* possibleName);

    char* GetName();

    void  SetName(char* newName);

         ~Named();

};

注意,NamedNumber 以公有方式同时继承了Number 和Named 类。另外需要注意,在 NamedNumber 的构造函数中,需要向两个基类都传入适当的构造函数参数。由于NamedNumber 并不具有自己独有的私有数据,所以,它自己的构造函数中没有任何代码。

通过以下示例代码,可观摩到NamedBehavior 类中被组合起来的行为。在这砣代码中,会声明一个NamedNumber,并且对它进行操作。

    NamedNumber time(0, "Elapsed Time");

     ...

     time.Next();                           // 从Number 类中继承的方法

     if (time.IsNamed("Elapsed Time") )...  // 从Named 类中继承的方法

     ...

     cout << time.GetName() << " is " << time.Value() << endl;

在这个示例代码中,NamedNumber对象,会对它从Number 基类中继承到的方法(Next、Value)进行响应,同样地,也会对它从Named 基类中继承到的方法(IsNamed、GetName)进行响应。

Named 类,可在多种上下文中使用。其它的具有名字的实体还包括Frame(帧)、Canvas(画布)和Button(按钮)。妳不需要在这些类中都各自包含一个名字属性,只需要利用多重继承来从Named 类中得到这个属性即可。在这种做法下,不但得到了编程效率提升的好处,而且,由于复用,还通过Named 类提供了一个统一的方法集合。这样,记忆起来就容易多了,一个对象的名字,可通过GetName 方法来获取,而不是Frame 类中的getName 方法,也不是Button 类中的NameIs 方法。

2 将接口与实现组合起来

多重继承,还可以用来增强 分离性 。分离性,是良好的软件工程和面向对象设计中的最基本的原则,并且,在类的结构中都可以观察到这一点,在类的结构中,其接口和实现是分离的。在多态性中也体现了分离性,因为,用来对其进行访问的类,与实际对其进行操作的对象的类型信息之间,是分离的;在这种情况下,分离性是由基类提供的。多态中的基类提供了一定程度的分离性,不过,访问类仍然被限制为,仅仅能够使用从特定的基类中派生的类。

由基类方式得到的分离性,其限制,可由下面的Clock 类的定义中看到。这个类的对象,可以与任何一个派生自 DisplayableNumber 的对象建立关联关系。通过多态性,可以实现,Clock对象无需知道(或者说,与后面这个信息相分离)它所关联到的那个对象的具体类型。


再次观摩Clock

class Clock

{

   private:

      DisplayableNumber *number; // 仅仅知道基类

  public:

     void ConnectTo(DisplayableNumber* dn);

      void ConnectTo(DisplayableNumber& dn);

      void Notify();

};

// 位于实现代码文件

void Clock::ConnectTo (DisplayableNumber* dn) { number = dn; }

void Clock::ConnectTo (DisplayableNumber& dn) { number = &dn; }

void Clock::Notify() { number->Next(); // 调用派生类的方法

                       number->Show(); // 调用基类的方法 }

尽管Clock类狠有用,但是,它的可复用性,被它自己对于DisplayableNumber 基类的依赖所限制了。这样,就使得,那些需要使用Clock 对象的服务,而本身却不是 DisplayableNumber的对象,无法与Clock 相关联。 Animation 类,就是这样一个例子:Animation对象中,有一系列需要显示的图片,它通过这些图片来达到动画效果,这些图片必须按照特定的速率来显示。通过Clock 对象来控制动画,就是最直观的答案;在每个时间周期的末尾,Clock请求动画对象显示下一张图片。(Animation 类的概况显示如下。)然而Clock对象无法与Animation 对象关联起来,它只能与DisplayableNumber 对象关联。显然,让Animation 继承自DisplayableNumber,也不合适。另一种可选方案呢,就是,形成一个新的Clock 类,AnimationClock,它继承自Clock,并且可与Animation 对象进行关联。不过,这种实现方式下,也有缺陷,那就是,AnimationClock会继承到不需要的数据和操作(也就是说,那些与DisplayableNumber 相关的东西)


Animation

class Animation

{

  public:

    Animation(...);

    ...

    void Next();     // 生成动画序列中的下一张图片;

                     // 并让它成为当前要显示的图片

   void Show();     // 显示当前图片

    ...

};

再细致地观察一下Clock 类,就可以发现,通过多重继承能够进一步增加分离性。Clock类只依赖狠少的东西:它仅仅需要调用Next 和Show 方法。这两个方法,都在 DisplayableNumber 的子类中有定义, 同时,在Animation 类的接口中也有这两个方法。此处,我们所需要的,就是,某种用来将DisplayableNumber 和Animation 类的相同属性通用化的技巧。继承,是基于通用性的一种技巧,并且,由于此处牵涉到了多个基类,所以,多重继承是一种可能的解决方法。

我们所期望的分离性,可通过以下方式来实现:通过多重继承,将一个纯抽象基类,与一个提供了其中抽象方法的实际实现的类,组合起来。回忆一下,这个术语,抽象类,指的是,某个基类,其中拥有至少一个纯虚方法。纯抽象类,指的是,其中所有的方法都是纯虚方法。在Clock 示例中所需要的纯抽象基类展示如下。


纯虚基类

class Sequenced       // 纯虚类

{

  public:

    void Next() = 0;  // 计算序列中的下一个元素

    void Show() = 0;  // 显示序列中的当前元素

};

Sequenced类,结合了DisplayableNumber 类和Animation 类之间的相似性。其中的纯虚方法,能够定义出这些相似性,而同时又无需在Sequenced 类中实际地提供任何实现。具体对这些纯虚方法进行实现的责任,就落到了那些派生自Sequenced 类的类头上。

这样,就可以重新定义Clock类,如下所示,让它只依赖Sequenced 类,而不依赖DisplayableNumber 类。这种变动,就提升了Clock 类的可复用性,因为,它会使得 Clock 对象能够与更广泛范围的对象建立关联关系,只要对方是继承自Sequenced 即可。这种变动同时也提升了Clock 类的清晰度,因为,它更直观且简洁地表达出了,Clock 对象对于它所要关联的对象是怎样的一个预期。


再次观摩Clock

class Clock

{

   private:

      DisplayableNumber *number; // 仅仅知道基类

  public:

     void ConnectTo(DisplayableNumber* dn);

      void ConnectTo(DisplayableNumber& dn);

      void Notify();

};

// 位于实现文件

void Clock::ConnectTo (DisplayableNumber* dn) { number = dn; }

void Clock::ConnectTo (DisplayableNumber& dn) { number = &dn; }

void Clock::Notify() { number->Next(); // 调用派生类中的方法

                       number->Show(); // 调用基类中的方法 }

Sequenced类,可以以三种方式来与DisplayableNumber 类层次组合起来:

  • •.DisplayableNumber,可以利用单重继承来继承Sequenced,这样,就可以将DisplayableNumber 的任意子类都当成Sequenced 实体来对待。这种方式,狠简单,同时,对 于DisplayableNumber 类层次的影响也最大。

  • •.DisplayableNumber 的各个子类,可以利用多重继承来同时继承DisplayableNumber 和Sequenced。这种方式,使得,类层次中的部分成员被当成Sequenced 实体对待,这样就能够表达出一个意图,即,只有部分的DisplayableNumber对象才能与Clock 关联起来。

  • •.新的类,可利用多重继承来同时继承DisplayableNumber 的某个子类和Sequenced。这样,就可以更好地利用Sequenced 类的复用性和灵活性,而同时无需修改任何已有的代码。如果已有的代码是位于某个库中,无法被修改,那么就有必要使用这种方式。



类似地,Animation类可利用单重继承直接继承自Sequenced 类,或者,也可以定义一个新的类并且以多重继承的方式使用。

下面定义了TimedNumber 和TimedAnimation 类,展示了以多重继承方式定义新的类的处理方式。TimedNumber类和TimedAnimmation类的名字,是有用意的,用来反映这样一个意图,即,它们要用来与修改过后的Clock 类相关联。


以多重继承方式来使用 Sequenced

class TimedNumber : public Number, public Sequenced

{

  public:

    TimedNumber(int initialValue);

};

class TimedAnimation : public Animation, public Sequenced

{

  public:

    AnimationSequence(...);

};

在TimedNumber 和TimedAnimation 类的定义中,关键的一点就是,它们以何种方式来满足由Sequenced 类所定义的责任。Sequenced 类中的纯虚方法,就要求其派生类必须实现Next 和Show 方法。TimedNumber通过继承Number 类来满足这个要求。TimedAnimation 类也是以类似的方式来满足这个要求。

现在,就可以创建Clock 与TimedNumber 对象以及与TimedAnimation 对象之间的关联了。可按照以下代码来创建关联:

    Clock slowTimer("Slow", 1000);

     Clock fastTimer("Fast", 50);

     ...

     TimedNumber count(0);

     TimedAnimation *movie = new TimedAnimation(...);

     ...

     slowTimer.ConnectTo( (Sequenced&)count );

     fastTimer.ConnectTo( (Sequenced*)movie );

这砣代码中,关键的点是,要能够将TimedNumber 和TimedAnimation 对象类型转换成Sequenced 类型。这种类型转换是有效的,因为TimedNumber 和TimedAnimation 都是继承自Sequenced 类。

最后一点,引入了Sequenced 类之后,就使得,Clock 类能够与未来可能定义的其它类关联起来。例如,可能会定义一个Sampler 类,它的作用是,定时报告某种资源的状态。比如,文件中有多少部分已经通过网络发送了,或者,某个文档中还有多少内容需要打印。利用多重继承和Sequenced 类,就不需要对Clock 类进行任何改变,就可以让Sampler 对象在Clock 对象的推动下工作。

3 练习

  1. 1.定义并实现一个Linked 混入类,它定义的是,单向链接的列表的属性。定义并实现一个简单的Number 类,用于测试。利用多重继承,定义、实现并测试一个LinkedNumber 类,它将妳的Number 和Linked 类的能力组合起来。

  2. 2.研究DisplayableNumber类中的其它方法,以确认,其中是否有任何方法也能够合理地放置到Animation 类中。然后这个结论,对Sequenced 类和Animation 类的定义进行修改。

未知美人

钟欣桐

未知美人

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands