如何基于多态清晰地处理不同的行为

时间:2011-03-24 20:38:55

标签: .net vb.net oop polymorphism

假设我有一个实现类IFooVideoFooAudioFoo的接口TextFoo。进一步假设我无法修改任何代码。假设我想根据IFoo的运行时类型编写一个行为不同的函数,例如

Public Class Bar
    Public Shared Sub Fix(ByVal Foo as IFoo)
        If TypeOf Foo Is VideoFoo Then DoBar1()
        If TypeOf Foo Is AudioFoo Then DoBar2()
        If TypeOf Foo Is TextFoo Then DoBar3()

    End Sub
End Class

我想重构这个以使用重载方法:

Sub DoBar(ByVal foo as VideoFoo)
Sub DoBar(ByVal foo as AudioFoo)
Sub DoBar(ByVal foo as TextFoo)

但我认为做这样的事情的唯一方法就是写

Sub DoBar(ByVal foo as IFoo)

然后我必须再做一次“If TypeOf ...... Is”。我怎样才能重构这个以利用IFoo实现的多态性而无需手动检查类型?

(在VB.NET中,虽然我的问题也适用于C#)

3 个答案:

答案 0 :(得分:3)

您要问的是Multiple Dispatch,或者是一种语言功能,它允许在运行时而不是编译时解析方法重载。

不幸的是,C#和VB.NET都是单调度语言,这意味着在编译时选择了方法重载。这意味着IFoo对象的重载将始终为IFoo选择,无论其实现类型如何。

然而,有很多方法可以解决这个问题。一种方法是使用访问者设计模式来实现双重调度,这将起作用。在C#中,您还可以使用新的dynamic关键字强制运行时环境在运行时解决重载。我写了一篇关于如何使用这种技术进行碰撞处理的blog entry,但它肯定适用于你正在做的事情。

我对VB.NET并不十分熟悉,但我相信如果将对象强制转换为Object,该语言默认会显示一些动态行为。如果这是错误的,请有人纠正我。

答案 1 :(得分:2)

嗯,一个选项是简单地重载Fix()方法,以便为每个实现IFoo的类型都有一个重载。但我怀疑你想直接接受界面,而不是它实现类型。

您实际需要的是multiple dispatch通常,C#/ VB使用参数的类型在编译时执行重载解析,并动态调度基于调用该方法的实例的运行时类型的调用。你想要的是在运行时根据参数的运行时类型执行重载解析 - 这是VB.NET或C#都不支持的功能。

过去,我一般使用由System.Type编制索引的代表字典来解决此类问题:

private readonly Dictionary<Type,Action<IFoo>> _dispatchDictionary;

static Bar()
{
    _dispatchDictionary.Add( typeof(TextFoo),  DoBarTextFoo );
    _dispatchDictionary.Add( typeof(AudioFoo), DoBarAudioFoo );
    _dispatchDictionary.Add( typeof(VideoFoo), DoBarVideoFoo );        
}

public void Fix( IFoo foo )
{
   Action<IFoo> barAction;
   if( _dispatchDictionary.TryGetValue( foo.GetType(), out barAction ) )
   {
      barAction( foo );
   }
   throw new NotSupportedException("No Bar exists for type" + foo.GetType());
}

private void DoBarTextFoo( IFoo foo ) { TextFoo textFoo = (TextFoo)foo; ... }
private void DoBarAudioFoo( IFoo foo ) { AudioFoo textFoo = (AudioFoo)foo; ... }
private void DoBarVideoFoo( IFoo foo ) { VideoFoo textFoo = (VideoFoo)foo; ... }

但是,从C#4开始,我们现在可以在C#中使用dynamic关键字来做同样的事情(VB.NET目前还没有这个功能):

public void Fix( IFoo foo )
{
    dynamic dynFoo = foo;
    dynamic thisBar = this;

    thisBar.DoBar( dynFoo ); // performs runtime resolution, may throw
}

private void Dobar( TextFoo foo ) { ... /* no casts needed here */ }
private void Dobar( AudioFoo foo ) { ... }
private void Dobar( VideoFoo foo ) { ... }

请注意,以这种方式使用dynamic关键字需要付出代价 - 它要求在运行时处理调用网站。它本质上是在运行时旋转一个版本的C#编译器,处理编译器捕获的元数据,执行类型的运行时分析,并吐出C#代码。幸运的是,DLR可以在第一次使用后有效地缓存这些呼叫站点。

作为一般规则,我发现这两种模式都令人困惑,并且在大多数情况下都是过度杀伤。如果子类型的数量很少且提前都知道它们,那么简单{ {1}}块可以更简单,更清晰。

答案 2 :(得分:0)

如果你不能改变界面,也不能改变任何类,那么按理说没有先前编写的代码可以利用你想要添加的这个新的修复功能。

我不知道VB.net,但我不禁想知道为什么你不简单地从每个当前类(和接口)中分类,并将你的新Fix方法放入子类。所有想要发送修复消息的新代码都应该接受IFixFoo而不是IFoo。

如果要在未创建的IFoo对象上调用Fix,则需要一种可以创建正确IFixFoo的方法。使用上面的代码,您只需要有一个地方可以执行If TypeOf ... Is(当您实际将IFoo转换为IFixFoo时。