C# 中的模拟多重继承模式






4.63/5 (51投票s)
用于在C#中模拟多重继承的设计模式。
引言
在设计程序或项目的某些模型时,存在多重继承是最佳选择(如果不是唯一选择)的情况。不幸的是,C# 不支持多重继承,因此我们必须寻找方法来适应我们的设计,使其能够实现多重继承。但C#确实提供了资源和技巧,可以在不彻底重新设计类模型的情况下模拟多重继承,只需添加几个辅助方法和类,并在编码时注意一两个细节即可。我提出了一种设计模式,它允许我们在C#程序中模拟多重继承,从而产生的类几乎就像真正继承自两个或多个父类一样。我们还将面临经典的多重继承问题,并探讨如何解决它们。
背景
大多数现代语言,如C#、Java、Delphi,都采用单一继承。它们不支持多重继承,因为其设计者不得不在引入多重继承及其带来的所有问题,与摒弃它并引入更通用、问题更少的接口和接口继承之间做出选择。多重继承会带来许多问题,我们稍后会讨论。但这些问题相当特殊,在任何可以使用多重继承或最适合采用多重继承进行设计的程序中都不会出现。
有传统的方法可以或多或少成功地通过接口继承来模拟多重继承。假设我们有两个类 `A` 和 `B`,我们希望 `C` 继承自它们两个。
class A
{
m1();
}
class B
{
m2();
}
class C : A, B
{
m1();
m2();
}
在C#中,这样的代码是不可能的。以下是处理这种情况的经典解决方法:
class A
{
m1();
}
interface IB
{
m2();
}
class B : IB
{
m2();
}
class C : A, IB
{
B BObject;
m1();
m2() { BObject.m2(); }
}
在此代码中,我们将类 `B` 实现一个名为 `IB` 的新接口,该接口拥有与 `B` 相同的成员。然后,类 `C` 继承自 `A` 和 `IB`,并使用一个内部的 `B` 对象来复制 `B` 的 `m2` 方法实现。因此,`C.m2` 实际上调用的是其 `BObject.m2` 方法。这样,我们可以说 `C` 现在拥有 `A` 的 `m1()` 方法实现和 `B` 的 `m2()` 方法实现。我们可以在任何需要 `A` 对象或 `IB` 对象的地方使用 `C` 对象。
但这种解决方案存在一些问题。其中一个问题是,我们不能在明确需要 `B` 对象的地方使用 `C` 对象,只能在需要 `IB` 对象的地方使用。因此,我们可能需要更改项目中的所有其他代码,将对 `B` 对象的引用替换为对 `IB` 对象的引用。如果我们正在设计模型并可以做出这样的决定,这不算大问题。但如果项目依赖于第三方代码或标准库(Framework),我们就无法进行这些修改。更重要的是,如果类 `B` 不是我们自己的,而是来自标准库或第三方代码,我们就无法让它实现我们的 `IB` 接口。我们无法更改它。
C# 的模拟多重继承模式
我们已经看到,仅使用接口和单一继承无法完全模拟多重继承。我们需要更多,而C#恰好拥有这些。让我们来看看。
我们的目标是
- 我们希望类 `C` 继承自类 `A` 和 `B`,能够调用它们的方法实现,而无需重写它们。
- 我们希望能够将 `C` 对象用在任何需要 `A` 对象或 `B` 对象的地方。
- 我们不想修改 `A` 或 `B`,因为出于某种原因它们是不可触及的,或者我们根本不在乎。
- 我们希望像普通对象一样实例化、引用和使用 `C` 对象。
- 我们处理的父类不公开
public
字段,而是使用属性。然而,即使其中一个父类(最多一个)公开public
字段,此模式也同样有效。 - 当然,此模式不仅适用于两个父类,也适用于三个或更多父类。
基本思想如下:
我们将创建两个辅助类,`Aaux` 和 `Baux`,它们分别继承自 `A` 和 `B`。
class A
{
m1();
}
class B
{
m2();
}
class Aaux : A
{
m1();
}
class Baux : B
{
m2();
}
我们的新类 `C` 不会继承自 `A` 或 `B`,但会拥有它们的所有成员。此外,它将包含两个对象:一个类型为 `Aaux`,另一个类型为 `Baux`。我们将分别称它们为 `C.APart` 和 `C.BPart`。`C` 将使用它们 `m1` 和 `m2` 的实现,而不是重写它们。
class C
{
Aaux APart;
Baux BPart;
m1()
{
APart.m1();
}
m2()
{
BPart.m2();
}
}
因此,每个 `C` 对象内部都有一对 `A` 和 `B` 对象。让这些对象知道谁在包含它们,通过添加对包含它们的 `C` 对象的引用。我们将为此目的修改 `Aaux` 和 `Baux` 类。
class Aaux : A
{
C CPart;
m1();
}
class Baux : B
{
C CPart;
m2();
}
最后,我们终于迎来了最终的技巧。我们将重新定义类 `C` 的隐式转换运算符,这样:
- 每当期望 `A` 对象而找到 `C` 对象时,将返回 `C.APart`。
- 每当期望 `B` 对象而找到 `C` 对象时,将返回 `C.BPart`。
同样,我们将重新定义 `Aaux` 类的隐式转换运算符,以便每当期望 `C` 对象而找到 `Aaux` 对象时,返回 `Aaux.CPart`。同样,我们将重新定义 `Baux` 类的隐式转换运算符,以便每当期望 `C` 对象而找到 `Baux` 对象时,返回 `Baux.CPart`。
最终效果如下:
class Aaux : A
{
C CPart;
m1();
static implicit operator C(Aaux a)
{
return a.CPart;
}
}
class Baux : B
{
C CPart;
m2();
static implicit operator C(Baux b)
{
return b.CPart;
}
}
class C
{
Aaux APart;
Baux BPart;
m1()
{
APart.m1();
}
m2()
{
BPart.m2();
}
static implicit operator A(C c)
{
return c.APart;
}
static implicit operator B(C c)
{
return c.BPart;
}
}
有了这段代码,我们就可以将 `C` 对象用在任何需要 `A` 或 `B` 对象的地方,以及需要 `C` 对象的地方。唯一的代价是增加了两个额外的类,并且要求父类不公开 public
字段。
然而,我们可以采取另一个步骤,这将使我们能够减少所需的额外类的数量,并允许其中一个父类拥有 public
字段。事实上,类图将更简单。
我们只需要让 `C` 直接继承自 `A`,即那个公开 public
字段的类。
当然,属性与此模式完全兼容,因为它们表现得像方法。所以父类可以拥有任意数量的 public
属性。
使用代码
我们来看一个例子。我们是一家电脑经销商,从主要供应商那里购买电脑硬件并销售给最终用户。然而,我们经常缺货,为了避免失去客户,我们会从竞争对手那里购买并转售给我们的客户。其他商店也采取同样的政策,所以其他商店也经常购买我们的商品以供以后转售。我们的程序有两个数组——一个数组存放我们所有的供应商,另一个数组存放我们所有的客户。竞争对手既是供应商也是客户。这就是想法:
class Vendor
{...}
class Customer
{...}
class Shop : Vendor, Customer
{...}
最终代码(可在下载中找到)可能是这样的:
/// <summary>
/// A computer manufacturer. They resupply us.
/// </summary>
public class Vendor
{
string id;
public string VendorId
{
get
{
return id;
}
set
{
id = value;
}
}
public Vendor(string vendorId)
{
id = vendorId;
}
public virtual void AskForRessuply()
{
Console.WriteLine("Please ressuply me, vendor "+id+".");
}
}
/// <summary>
/// A customer. We send them their purchased goods.
/// </summary>
public class Customer
{
string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public Customer(string customerName)
{
name = customerName;
}
public virtual void SendOrder()
{
Console.WriteLine("Dear "+name+": We are sending your goods.");
}
}
/// <summary>
/// The auxiliary class that redefines Customer.
/// </summary>
internal class CustomerAux : Customer
{
// <--- It has a link to the Shop object that contains it.
internal Shop shopPart;
internal CustomerAux(string customerName) : base (customerName)
{
}
// We declare the implicit casting operator for returning
// shopPart when a Shop object is expected.
static public implicit operator Shop(CustomerAux c)
{
return c.shopPart;
}
}
/// <summary>
/// We consider a shop like a Vendor and a Customer,
/// for we both ask them to resupply us,
/// or they purchase our goods.
/// </summary>
public class Shop : Vendor // <-- It inheirs only from Vendor...
{
CustomerAux customerPart; // ...but has a CustomerAux object inside.
// Shops have an address in addition to the vendor id
// and the customer name inherited from Vendor and Customer.
string address;
public string Address
{
get
{
return address;
}
set
{
address = value;
}
}
// Here we are 'redirecting' property Name to the customerPart object.
public string Name
{
get
{
return customerPart.Name;
}
set
{
customerPart.Name = value;
}
}
// The Shop constructor.
public Shop(string vendorId, string customerName, string shopAddress) :
base (vendorId)
{
// We create and bind the CustomerAux object to this one.
customerPart = new CustomerAux(customerName);
customerPart.shopPart = this;
address = shopAddress;
}
// Here we are redirecting Customer.SendOrder to the customerPart object.
public virtual void SendOrder()
{
customerPart.SendOrder();
}
// We redefine the implicit casting operator for returning
// customerPart when a Customer object is expected.
static public implicit operator Customer(Shop s)
{
return s.customerPart;
}
}
// An example of use of the Vendor, Customer and Shop classes.
class EntryPoint
{
static void Main(string[] args)
{
Vendor ibm = new Vendor("32FK-IBM");
Vendor hp = new Vendor("1138-HP");
Customer mrSimpson = new Customer("Mr. Simpson");
Customer mrGates = new Customer("Mr. Gates");
Shop joys =
new Shop("1979-JCS", "Joy's Computer Shop", "123, Fake St.");
Vendor[] vendors = {ibm, hp, joys};
foreach(Vendor ven in vendors)
ven.AskForRessuply();
Customer[] customers = {mrSimpson, mrGates, joys};
foreach(Customer cus in customers)
cus.SendOrder();
Console.ReadLine();
}
}
值得关注的点 - 经典问题
多重继承最重要的问题之一是由这种情况引起的:
类 `A` 拥有 `m1()` 方法。`[ImpA]` 表示 `m1` 方法已在 `A` 中实现。类 `B` 和 `C` 继承自 `A`,并且它们都重新定义了 `m1` 方法。因此,类 `B` 拥有自己的 `m1` 实现,表示为 `[ImpB]`,类 `C` 也拥有自己的 `m1` 实现,表示为 `[ImpC]`。
现在,类 `D` 继承自 `B` 和 `C`。问题是……运行此代码时使用哪个 `m1` 的实现?
D d = new D();
d.m1();
……此代码?
B d = new D();
d.m1();
……以及此代码?
C d = new D();
d.m1();
支持多重继承的语言和编译器会以某种方式解决这个问题。但这会使编译、调试和理解代码变得更加困难。 `m1` 实现的版本几乎是不可预测的。
然而,此模式在一定程度上解决了这个问题,因为我们可以选择使用哪个实现。假设我们的 `D` 类使用了我们模拟多重继承的模式,将有五种不同的情况:
- `D` 重新定义了 `m1`,并且我们总是希望使用它的实现:我们只需要让 `D.BPart.m1()` 和 `D.CPart.m1()` 调用 `D.m1()`。
- 我们总是希望使用 `B` 的实现:然后,我们让 `D.m1()` 和 `D.CPart.m1()` 调用 `D.BPart.m1()`。
- 我们总是希望使用 `C` 的实现:然后,我们让 `D.m1()` 和 `D.BPart.m1()` 调用 `D.CPart.m1()`。
- 我们希望在从 `B` 类型变量调用 `m1` 时使用 `B` 的实现,在从 `C` 类型变量调用 `m1` 时使用 `C` 的实现,而在从 `D` 类型变量调用时使用 `D` 的新实现:只需在 `D` 类中重新定义 `m1`,而不要动 `D.BPart` 和 `D.CPart`。(此选项不推荐,除非我们知道自己在做什么。)
- 我们希望根据调用时引用 `D` 对象的变量类型,有三种不同的新实现:我们在 `D.m1()`、`D.BPart.m1()` 和 `D.CPart.m1()` 中编写不同的实现。(这比前一种情况更不推荐。但它可能对某人有用。)
最后评论
C# 没有包含多重继承是有重要原因的。然而,我有时会想念它,这正是我提出这个模式的原因。我认为它可能对那些在其应用中发现比缺点更多的优点的人有用。我希望看到你们对此的评论,指出我没有看到的缺点以及改进建议。
历史
- 2005年4月9日:本文初版。