在这种情况下,当基类的实现被修改之后,派生类将无法正常运行而必须被修改。对于一个中等规模的C++程序而言,这或许并不成为一个问题,因在这种情况下,我们一般能够获取所有的源代码,因此也将能够对派生类进行修改。但是对于较大的C++程序而言,对受影响的派生类进行修改所花费的时间将是相当长的。更糟的是,我们可能根本无法得到所有的源代码。这就是为什么一些用C++编写大型程序的专家们强烈建议人们基于抽象基类来构建应用程序。
抽象基类是一种最纯粹的接口继承,并且正好也被用来实现COM接口。由于任何人可以用任何语言在任何地方编写COM组件,因此必须有一种非常严格的方法来保证组件的客户不会因组件的变化而受到影响。显然实现继承无法提供客户所需的保护。
因此,为了保证对组件的修改不会影响应用程序的正常运行,COM并不支持实现继承。 但这并不会使COM的功能有任何损失,因我们可以用组件包容来完全模拟实现继承。在C++中,对类的改造是用包容和继承来实现的。在COM中,则可使用内容和聚合来对组件进行改造。包容和聚合实际上是使一个组件使用另外一个组件的一种技术。
聚合的目标就是使客户确信内部组件所实现的某个接口是由外部组件实现的。此时需要从内部组件中将一个指针直接传给客户并使客户相信此接口指针是属于外部组件的。若将内部组件按通常方式实现的接口指针传给客户,那么客户得到的是对于组件的一个分裂的视图。也就是说内部组件的接口将调用内部组件所实现的QueryInterface,而外部组件则有其自己的QueryInterface。当客户查询属于内部组件的接口时,它所获得的关于组件的功能试图与它查询外部组件的接口时是不同的。
用一个例子来说明这个问题可能会更加清楚。假定有某个聚合组件。其中外部组件支持接口IX和IY。它自己实现了IX接口,而IY接口是聚合得到的。内部组件实现了IY和IZ接口。在创建了外部组件之后,我们将可以看到起IUnknown接口指针。用此接口指针可以成功地查询IX或IY,但查询IZ时将返回E_NOINTERFACE。当查询IY接口时,得到的实际上是内部组件的IY接口指针。若使用此IY指针查询IZ,此查询将会成功。这是由于IY接口的IUnknown函数时内部组件实现的。与此类似,通过IY接口查询IX将会失败,这是由于内部组件并不支持IX。 这样一来,关于QueryInterface实现的一条基本规则,即“若能够从某接口获取某特定接口,则从任意接口都将能够获取此接口”。内部组件使用外部未知接口最简单的方法是将调用请求转发给外部未知接口。为此,内部组件需要一个指向外部未知接口的指针。并且它还需要知道它是被聚合的。
组件的复用同将一个组件包容在另外一个组件中是一样简单的。当需要对组件进行定制时,可以在调用其成员函数的前后加上自己的代码。
如果只是希望给组件增加新的接口,那么可以使用聚合的方法。在作者看来,聚合实际上是包容的一个特例。当外部组件聚合内部组件的某个接口时,外部组件可以将接口指针直接传给客户,而不需要重新实现此接口或转发相应的函数调用。