DEV Community

Cover image for Double dispatcher in C#
Ashiqul
Ashiqul

Posted on

Double dispatcher in C#

Double dispatcher is a feature of some programming languages that allows the runtime to decide which method to invoke/dispatch based on passed arguments type.

Single dispatch

In all C-like programming languages, like C#, virtual is used to facilitate polymorphic behavior. Usage of virtual is a special type of dispatch know as single dispatch. In single dispatch which method will be invoked is decided based on the invoker object's type which is known only in runtime, not compilation time. So for following hierarchy order.IssueOrder(...) will invoke .IssueOrder(..) from type PurchaseOrder provided that var order = new PurchaseOrder().

public interface IOrder
{
    Result IssueOrder(IOrder order);
}
public class SalesOrder : IOrder
{
    public Result IssueOrder(IOrder order) => Result.Ok();
}
public class PurchaseOrder : IOrder
{
    public Result IssueOrder(IOrder order) => Result.Fail("failed");
}
Enter fullscreen mode Exit fullscreen mode

Why single dispatch is not enough?

Consider the following service:

public class OrderIssuerService
{
    public string Issue(IOrder order) => Do(order);

    private string Do(IOrder _) => nameof(IOrder);
    private string Do(PurchaseOrder _) => nameof(PurchaseOrder);
    private string Do(SalesOrder _) => nameof(SalesOrder);
}
Enter fullscreen mode Exit fullscreen mode

What will be the output of the following code block?


//  private readonly ITestOutputHelper _outputHelper;

[Fact]
public void Order_Issuer_Service_Should_Invoke_Method_Taking_IOrder_As_Argument()
{
    _outputHelper.WriteLine(new OrderIssuerService().Issue(new SalesOrder()));
    _outputHelper.WriteLine(new OrderIssuerService().Issue(new PurchaseOrder()));
}
Enter fullscreen mode Exit fullscreen mode

Output

IOrder
IOrder
Enter fullscreen mode Exit fullscreen mode

The important point to note is none of the overloads that take PurchaseOrder or SalesOrder has been invoked. That's because method invocation in C# doesn't depend on the passed argument's type only on the invoker's type.

How can we make C# to invoke the overloads for PurchaseOrder and SalesOrder?

Approach one: Check for order arguments real type as following

public string Issue(IOrder order)
{
    return order.GetType() == typeof(PurchaseOrder)
        ? Do(order as PurchaseOrder)
        : Do(order as SalesOrder);
}
Enter fullscreen mode Exit fullscreen mode

But checking a type like this is not recommended. It in fact points out a flaw in the design but that's a different topic. So we don't want to cast as such what can we do?

Approach two: Use dynamic keyword

public class OrderIssuerService
{
    public string Issue(dynamic order) => Do(order);

    ...
    ...
}
Enter fullscreen mode Exit fullscreen mode

In this case, C# runtime will invoke the proper overload because dynamic variable holds the original type which is PurchaseOrder/SalesOrder.

Is dynamic recommended to be used in this case?

No. dynamic is generally not a good approach to solve this problem. There are other ways to resolve the issue using visitor pattern but dynamic works :)

An example where employing double dispatcher can be valuable

Consider the following scenario:

public class OrderIssuer
{
    public void Issue(IOrder order) 
    { 
        // complex legacy logic

        MutateOrder(order); 

        // complex legacy logic
    }

    private void MutateOrder(IOrder _) { // complex legacy logic }
}
Enter fullscreen mode Exit fullscreen mode

Let's say there's a bug in the method MutateOrder but it's legacy code and you don't want to touch any of the methods now (maybe your boss forbade you to do so or you are on a deadline). If you know the fix is related to type PurchaseOrder then maybe following can work for you (of course it depends on the code in MutateOrder)

public class OrderIssuer
{
    public void Issue(IOrder order) 
    { 
        // complex legacy logic

        MutateOrder(order); 

        // will invoke the new overload as order is of 
        MutateOrder((dynamic)order); 

        // complex legacy logic
    }

    private void MutateOrder(IOrder _) { // complex legacy logic }
    private void MutateOrder(PurchaseOrder _) { // new logic }
}
Enter fullscreen mode Exit fullscreen mode

This approach obviously has the downside that for SalesOrder 1st overload will be invoked twice but the point is without changing anything in the legacy code we might be able to fix the bug albeit the approach is a dirty approach.

Discussion (1)

Collapse
wahidd profile image
David Wahid

Thorough explanation