在我看来,与其解决这个问题,不如重新考虑你的设计。您绝对确定在这种情况下不能使用多态性 - 要么直接让实体负责您尝试执行的操作,要么使用访问者模式。我几次遇到这个问题,并且总是决定改变设计 - 它导致了更清晰的代码。我建议您也这样做,除非您完全确定依赖类型是最好的解决方案。
问题
为了让示例至少与现实世界有一些相似之处,我们假设您有以下实体:
public abstract class Operation
{
public virtual DateTime PerformedOn { get; set; }
public virtual double Ammount { get; set; }
}
public class OutgoingTransfer : Operation
{
public virtual string TargetAccount { get; set; }
}
public class AtmWithdrawal : Operation
{
public virtual string AtmAddress { get; set; }
}
它自然是更大模型的一小部分。现在您面临一个问题:对于每种具体类型的操作,都有不同的显示方式:
private static void PrintOperation(Operation operation)
{
Console.WriteLine("{0} - {1}", operation.PerformedOn,
operation.Ammount);
}
private static void PrintOperation(OutgoingTransfer operation)
{
Console.WriteLine("{0}: {1}, target account: {2}",
operation.PerformedOn, operation.Ammount,
operation.TargetAccount);
}
private static void PrintOperation(AtmWithdrawal operation)
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
operation.PerformedOn, operation.Ammount,
operation.AtmAddress);
}
简单的重载方法将在简单的情况下工作:
var transfer = new OutgoingTransfer
{
Ammount = -1000,
PerformedOn = DateTime.Now.Date,
TargetAccount = "123123123"
};
var withdrawal = new AtmWithdrawal
{
Ammount = -1000,
PerformedOn = DateTime.Now.Date,
AtmAddress = "Some address"
};
// works as intended
PrintOperation(transfer);
PrintOperation(withdrawal);
不幸的是,重载方法是在编译时绑定的,因此一旦引入数组/列表/任何操作,只会调用通用(操作操作)重载。
Operation[] operations = { transfer, withdrawal };
foreach (var operation in operations)
{
PrintOperation(operation);
}
这个问题有两种解决方案,但都有缺点。您可以在操作中引入抽象/虚拟方法来将信息打印到选定的流。但这会将 UI 问题混合到您的模型中,因此这对您来说是不可接受的(稍后我将向您展示如何改进此解决方案以满足您的期望)。
您还可以以以下形式创建大量 if:
if(operation is (ConcreteType))
PrintOperation((ConcreteType)operation);
这个解决方案很丑陋并且容易出错。每次添加/更改/删除操作类型时,您都必须检查使用这些 hack 的每个地方并对其进行修改。如果您错过了一个地方,您可能只能捕获该运行时 - 对某些错误(例如缺少一种子类型)没有严格的编译时检查。
此外,一旦引入任何类型的代理,该解决方案就会失败。
代理如何工作
下面的代码是非常简单的代理(在这个实现中,它与装饰器模式相同 - 但这些模式通常并不相同。需要一些额外的代码来区分这两种模式)。
public class OperationProxy : Operation
{
private readonly Operation m_innerOperation;
public OperationProxy(Operation innerOperation)
{
if (innerOperation == null)
throw new ArgumentNullException("innerOperation");
m_innerOperation = innerOperation;
}
public override double Ammount
{
get { return m_innerOperation.Ammount; }
set { m_innerOperation.Ammount = value; }
}
public override DateTime PerformedOn
{
get { return m_innerOperation.PerformedOn; }
set { m_innerOperation.PerformedOn = value; }
}
}
正如您所看到的 - 整个层次结构只有一个代理类。为什么?因为您应该以不依赖于具体类型的方式编写代码 - 仅依赖于提供的抽象。该代理可以及时推迟实体加载 - 也许您根本不会使用它?也许您只会使用 1000 个实体中的 2 个?那为什么要加载它们呢?
因此 NHibernate 使用像上面这样的代理(不过更复杂)来推迟实体加载。它可以为每个子类型创建 1 个代理,但它会破坏延迟加载的整个目的。如果您仔细观察 NHibernate 如何存储子类,您会发现,为了确定实体是什么类型,您必须加载它。所以不可能有具体的代理——你只能有最抽象的,OperationProxy。
尽管 ifs 的解决方案很丑陋 - 但它是一个解决方案。现在,当您为问题引入代理时,它不再起作用。因此,我们只能使用多态方法,这是不可接受的,因为将 UI 责任混合到模型中。让我们解决这个问题。
依赖倒置和访问者模式
首先,让我们看看使用虚拟方法的解决方案是什么样子的(仅添加了代码):
public abstract class Operation
{
public abstract void PrintInformation();
}
public class OutgoingTransfer : Operation
{
public override void PrintInformation()
{
Console.WriteLine("{0}: {1}, target account: {2}",
PerformedOn, Ammount, TargetAccount);
}
}
public class AtmWithdrawal : Operation
{
public override void PrintInformation()
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
PerformedOn, Ammount, AtmAddress);
}
}
public class OperationProxy : Operation
{
public override void PrintInformation()
{
m_innerOperation.PrintInformation();
}
}
现在,当你打电话时:
Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
operation.PrintInformation();
}
一切都充满魅力。
为了消除模型中的 UI 依赖,让我们创建一个界面:
public interface IOperationVisitor
{
void Visit(AtmWithdrawal operation);
void Visit(OutgoingTransfer operation);
}
让我们修改模型以依赖于这个接口:
现在创建一个实现 - ConsoleOutputOperationVisitor (我已删除 PrintInformation 方法):
public abstract class Operation
{
public abstract void Accept(IOperationVisitor visitor);
}
public class OutgoingTransfer : Operation
{
public override void Accept(IOperationVisitor visitor)
{
visitor.Visit(this);
}
}
public class AtmWithdrawal : Operation
{
public override void Accept(IOperationVisitor visitor)
{
visitor.Visit(this);
}
}
public class OperationProxy : Operation
{
public override void Accept(IOperationVisitor visitor)
{
m_innerOperation.Accept(visitor);
}
}
这里会发生什么?当您在操作上调用 Accept 并传递访问者时,将调用 Accept 的实现,其中将调用 Visit 方法的适当重载(编译器可以确定“this”的类型)。因此,您可以结合虚拟方法和重载的“力量”来调用适当的方法。正如您所看到的 - 现在 UI 参考在这里,模型仅依赖于一个接口,该接口可以包含在模型层中。
现在,为了使其正常工作,需要实现该接口:
public class ConsoleOutputOperationVisitor : IOperationVisitor
{
#region IOperationVisitor Members
public void Visit(AtmWithdrawal operation)
{
Console.WriteLine("{0}: {1}, atm's address: {2}",
operation.PerformedOn, operation.Ammount,
operation.AtmAddress);
}
public void Visit(OutgoingTransfer operation)
{
Console.WriteLine("{0}: {1}, target account: {2}",
operation.PerformedOn, operation.Ammount,
operation.TargetAccount);
}
#endregion
}
和代码:
Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
operation.Accept(visitor);
}
我很清楚这不是一个完美的解决方案。添加新类型时,您仍然需要修改界面和访问者。但是您可以进行编译时检查,并且永远不会错过任何内容。使用此方法确实很难实现的一件事是获得可插入的子类型 - 但无论如何我不相信这是一个有效的场景。您还必须修改此模式以满足您在具体场景中的需求,但我会将其留给您。