Pergunta

I was going through old code and noticed one pattern that repeats itself all over the place - dynamic dispatch.

While I don't see any problem with the implementation itself, I was wondering if there are other ways of handling dynamic dispatch.

To bring an example - consider a scenario where at run time I'm going to get a message - either Customer or Visitor and perform some operation based on the type of the message. For the sake of brevity let's say all we need to do is to print out a corresponding message type.

So to go from words to code

public abstract class Message
{
   public abstract string Id {get;}
}

public class Customer: Message
{
   public override string Id {get {return "Customer";}}
}

public class Visitor: Message
{
   public override string Id {get {return "Visitor";}}
}

And handling part

public interface IHandleMessage
{
  bool CanHandle(Message message);
  void Handle(Message message);
}

public class CustomerHandler: IHandleMessage
{
   public bool CanHandle(Message message)
   {
      return message.Id == "Customer";
   }

   public void Handle(Message message) {Console.WriteLine(message.Id);}
}

public class VisitorHandler: IHandleMessage
{
   public bool CanHandle(Message message)
   {
      return message.Id == "Visitor";
   }

   public void Handle(Message message) {Console.WriteLine(message.Id);}
}

public class MessageHandlersFactory
{
   private static readonly IEnumerable<IHandleMessage> _messageHandlers =
       new List<IHandleMessage> {new CustomerHandler(), new VisitorHandler()};

   public static IHandleMessage GetHandlerForMessage(Message message)
   {
       return _messageHandlers.FirstOrDefault(h => h.CanHandle(message));
   }
}

And the client code looks like this

public class Program
{
   public static void Main(Message message)
   {
      IHandleMessage handler = MessageHandlersFactory.GetHandlerForMessage(message);

      handler.Handle(message)
   }
}

Now as I said, I don't mind this implementation, what I really don't like is this pattern repeats itself all over the place, which makes me think if this is not an overuse of dynamic dispatch.

To generalize - everywhere where decision has to be made based on incoming message/data/argument I do have more or less the same implementation as I provided above.

So my question is - given example above is there other ways of handling this kind of scenarios ? Or this is recommended and accepted way of dealing with dynamic dispatch?

Foi útil?

Solução

One could get rid of the CanHandle() method and simply use a dictionary where the key is the message Id. That would be simple and clean.

If the handling logic becomes more complex or if multiple handlers can be invoked for a particular message, then CanHandle() serves a better purpose.

But it seems from your example that only a single handler for each message is needed, so a dictionary of handlers would suffice. The factory could use the internal dictionary and return the correct handler class based on key (id).

Outras dicas

The goal of this is to ultimately pick a method to handle a message. You are currently emulating a Dictionary with CanHandle substituting for a key match. But for the conditions presented here, a standard dictionary will find the handler more efficiently.

When I've done this, instead of using an abstract class and a constant string message type, I've used a marker interface and the type itself as the message type. Something like this (from your classes above):

public interface IMessage { }
public class Customer : IMessage { }
public class Visitor : IMessage { }

This makes it pretty easy to define a dictionary-based class for handling any IMessage:

public class MainHandler
{
    private Dictionary<Type, Action<IMessage>> _handlers;

    // constructor
    public MainHandler(Dictionary<Type, Action<IMessage> handlers)
    {
        _handlers = handlers;
    }

    public void Handle(IMessage message)
    {
        Action<IMessage> handle = null;
        if (_handlers.TryGetValue(message.GetType(), out handle))
        {
            handle(message);
        }
        else
        {
            throw new NotImplementedException("No handler found");
        }
    }
}

Now the trick is to get the dictionary populated. You could do this using another marker interface for the handler, then find them with reflection. But I find it much clearer to manually list the handlers. That way it's easier to trace usage with standard tools, e.g. Find All References. You can also use helper functions to make it a little easier.

public class CustomerHandler
{
    public static void Handle(Customer message) { return; }
}
public class VisitorHandler
{
    public static void Handle(Visitor message) { return; }
}

public class Program
{
    // helper
    static KeyValuePair<Type, Action<IMessage>> ToHandle<T>(Action<T> action)
    {
        Type key = typeof(T);
        // build in cast to proper type to make it easier on handler code
        Action<IMessage> value = message => action((T)message);
        return new KeyValuePair<Type, Action<IMessage>>(key, value);
    }

    public static void Main()
    {
        // initialize things
        var mainHandler = new MainHandler(
            new List<KeyValuePair<Type, Action<IMessage>>>
            {
                // main stuff that changes here
                ToHandle<Customer>(CustomerHandler.Handle),
                ToHandle<Visitor>(VisitorHandler.Handle)

            }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));

        // probably happens in a loop
        IMessage message = ... ; // read message
        mainHandler.Handle(message);
    }
}

If you've made it this far, you're probably thinking static handler methods are not going to work because what about dependencies? But you can still provide dependencies to static methods by passing them in as parameters.

public class CustomerHandler
{
    public static void Handle(string setting, SharedResource res, Customer message) { ... }
}

public class VisitorHandler
{
    public static void Handle(string setting, Visitor message) { ... }
}

public class Program
{
    ...

    public static void Main()
    {
        // initialize things
        var res = new SharedResource();
        var setting = ConfigHelper.LoadSettingFromSomewhere();
        var mainHandler = new MainHandler(
            new List<KeyValuePair<Type, Action<IMessage>>>
            {
                ToHandle<Customer>(x => CustomerHandler.Handle(setting, res, x)),
                ToHandle<Visitor>(x => VisitorHandler.Handle(setting, x))

            }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));

        ...
    }

You could even create and pass in functions that you might need in the handler:

// initialize things
Func<SqlConnection> getConnection = () => new SqlConnection("...");
...
    ToHandle<Customer>(x => CustomerHandler.Handle(getConnection, ... , x));

In this implementation, the CanHandle boilerplate is eliminated. Handlers can be defined in any way you want (no interface to conform to). Then the wire-up gives you the opportunity to provide handlers with exactly the dependencies they need to fulfill their use case.

Dynamic dispatch, aka polymorphism, is one of he core components of object-oriented programming. And in an application that uses polymorphic objects, I would expect to see dynamic dispatch used everywhere.

However, what I wouldn't expect to see is code that immediately creates an object, makes a single polymorphic call on that object, and then throws the object away. And if that's what's happening in your code, I'd say that your object model doesn't need to be polymorphic, and probably doesn't benefit from polymorphism.

To give an example of what I consider "good" polymorphism: all investments (stocks, bonds, options, &c) have a price, a quantity, and a value that is calculated from those two attributes. However, the specific calculation depends on the investment type, as do the rules for trading the investment. So it makes sense to define a base Investment interface, with concrete subclasses. This interface defines the behaviors and interactions that the program can have with a generic investment -- it is an abstraction.

On the other hand, you might define an object model for processing events in which there is no need for an abstraction: you read an event and then hand it off for processing, with each event being processed by a completely distinct set of code. In this case, there's little need for a common interface that defines a process method.

That doesn't mean that an object-oriented design isn't still useful: objects are a useful tool for modularization. But they don't necessarily need to be polymorphic, and your factory method could create the object and immediately call its process function..

And if you do need polymorphism, be aware that any alternative such as a map of functors is simply moving the implementation of dynamic dispatch into your code, rather than letting the runtime handle it.

I think its popular because of its extensibility. But we should remember that at some point in your code you instantiate the Customer and Visitor objects.

You gloss over this in your example code, but it would be quite possible to have :

public class Customer
{
    public Customer(IHandler<Customer> handler)
    {
        this.handler = handler;
    }

    private IHandler<Customer> handler;
    public Handle() { handler.Handle(this);}
}

CanHandle and Handle can be combined into one method, returning true if handled.

What happens when two handlers are capable of handling the same message? Currently it will use the first one inserted into the list. This could be a source of differing behavior (i.e. bugs), if the handlers are dynamically created and inserted.

Object oriented programming environments often implement this type of dispatch in a simpler way:

  • Message identifiers are small integers, not strings.
  • Message handlers are function pointers (delegates in .net).
  • Dispatch consists of indexing an array of function pointers using the message identifier, finding the handling function, and invoking it.

You could use a similar algorithm.

Licenciado em: CC-BY-SA com atribuição
scroll top