How to cleanly deal with different behavior based

2019-06-22 08:14发布

问题:

Suppose I have an interface IFoo with implementation classes VideoFoo, AudioFoo, and TextFoo. Suppose further that I cannot modify any of that code. Suppose that I would then like to write a function that acts differently based on the runtime type of IFoo, such as

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

I would like to refactor this to use overloaded methods:

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

But the only way I see to do something like that would be to write

Sub DoBar(ByVal foo as IFoo)

Then I have to do my "If TypeOf ... Is" again. How can I refactor this to take advantage of the polymorphism of the implementations of IFoo without manually checking the types?

(in VB.NET, though my question applies to C# too)

回答1:

Well, one option is to simply overload the Fix() method so that you have one overload for each type implementing IFoo. But I suspect you want to accept the interface directly, rather than it's implementing types.

What you're actually looking for is multiple dispatch. Normally, C#/VB use the types of the argument(s) to perform overload resolution at compile time, and dynamic dispatch of the call based on the runtime type of the instance on which the method is called. What you want is to perform overload resolution at runtime based on the runtime types of the arguments - a feature that neither VB.NET or C# directly support.

In the past, I've generally solved this kind of problem using a dictionary of delegates indexed by 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; ... }

However, as of C# 4, we can now use the dynamic keyword in C# do essentially do the same thing (VB.NET does not have this feature as of yet):

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 ) { ... }

Note that using the dynamic keyword this way has a price - it requires that the call site be processed at runtime. It essentially spins up a version of the C# compiler at runtime, processes the metadata captured by the compiler, performs runtime analysis of the types, and spits out C# code. Fortunately, the the DLR can typicall cache such call sites effectively after their first use.

As a general rule, I find both of these pattern to be confusing, and overkill for most situations. If the number of subtypes is small and they are all known ahead of time, a simple if/else block can bemuch simpler and clearer.



回答2:

What you're asking about is Multiple Dispatch, or a language feature that allows method overload resolution at runtime instead of compile time.

Unfortunately C# and VB.NET are both single-dispatch languages, which means the method overload is chosen at compile time. This means that the overload for an IFoo object will always be chosen for IFoo, regardless of its implementing type.

There are ways around this however. One way is to use the Visitor design pattern to implement double-dispatch, which would work. In C# you can also use the new dynamic keyword to force the run-time environment to resolve the overload at run-time. I wrote a blog entry about how to perform collision handling using this technique, but it's certainly applicable to what you're doing too.

I'm not terribly familiar with VB.NET, but I believe the language exhibits some dynamic behaviours by default, if the objects are cast to Object. Someone please correct me if this is wrong.



回答3:

If you can't change the interface, nor any of the classes, then it stands to reason that no previously written code can take advantage of this new Fix function you want to add.

I don't know VB.net, but I can't help but wonder why you don't simply sub-class off of each of the current classes (and the interface,) and put your new Fix method in the sub-classes. All of your new code that wants to send the Fix message should accept an IFixFoo instead of an IFoo.

If you want to call Fix on IFoo objects that you didn't create, then you need a method that can create a correct IFixFoo. Using the above, you only have one place where you have to do the If TypeOf ... Is (when you actually convert an IFoo to an IFixFoo.