Clash of Styles, Part #5 – Double Dispatch, or When to Abandon OOP

Intro

Object-Oriented Programming is by far the most popular programming paradigm. OOP strengths are indisputable and well known, but it also has its weaknesses. In this article, I’ll show you that sometimes pure OOP can bring a lot of awkwardness to our program.

I hope that, by the end of the post, you will be convinced that there’s no right approach for everything. It’s all context-specific. Understanding that is is probably the most significant factor that distinguishes seasoned developers than less experienced ones.

So far, in this series, you’ve seen a lot of practical comparisons between OOP and FP. From the previous article, I’ve started digging even deeper and presented a new use case for our interpreter that we implemented gracefully by following Functional idioms. In this post, you’ll see some advanced OOP trickery called Double Dispatch. This technique is required to solve our problem in a pure OOP style.

The purpose of this post is not to diminish the power of OOP. Instead, it’s about adding more concreteness to our OOP vs. FP discussion and strengthening your knowledge and intuition on the pros and cons of the two paradigms.

As I’ve already mentioned – you need to have both of them in your arsenal. After all, using the right tool for the right job is at the heart of every elegant and maintainable software solution.

Note: This series of articles is very much influenced by some of the core ideas presented in the excellent three-part course “Programming Languages” on Coursera. I encourage you to read my review and pick up the course yourself.

Additionally, you can read through Chapter 6 – “Objects and Data Structures” of the bestseller Clean Code by Robert C. Martin. Some of the concepts presented here are also discussed in this great book. By the way, you should read the book anyway if you haven’t already.

Problem Recap

In the previous post, we added a new, Rational type to our interpreter by using standard Functional idioms like Pattern Matching and Discriminated Unions.

In other words, instead of being limited to expressions with only integer values like this one:

I extended the interpreter to support rational numbers so that I could build richer expressions like the following:

You’ve seen that the FP solution was quite straightforward. For a complete recap, please re-visit Part #4 or take a quick look at the final version of its’ source code here.

Now, it’s time to solve the same problem with OOP. Let’s see what it has to offer.

Adding the “Rational” Type in OOP

As you know already, adding the new Rational type means adding a new variant, or row, to our Operations Matrix:

This is very natural for OOP, like you saw in Part #3. You just need to add a new class and implement all of the operations on itself—no modifications of existing code. Open-Closed Principle followed to the full extend.

The complexity comes from a different place, though. The tricky part is when we try to make our Addition expression work not only with integers but also with rational numbers. Something that we did quite easily with F# and pattern matching.

The sections below will walk you through the full process and the required changes to accomplish that in pure OOP. If you find anything confusing and want to experiment with the code by yourself, feel free to review and download the final version.

Let’s start with our implementation journey.

Implementing “MyRational”

The starting point for my work in this article is the source code from Part #1, where I’ve implemented the initial version of the interpreter using OOP supporting only integers as value types. You can find the full source code here.

Now, let’s move on to adding support for rational numbers.

First, similarly to how I introduced a new Value type in F#, I’ll add a new IValue interface:

public interface IValue : IExpression
{
}

For now, this will be just a marker interface that will be “implemented” by the two value types – ints and rationals.

I need to change the Eval signature in IExpression to return IValue, as the result of an evaluation can now be both an integer or a rational:

public interface IExpression
{
    IValue Eval();
    
    string Stringify();
}

Also, MyInt class will now implement IValue instead of IExpression directly:

public class MyInt : IValue
{
    private int Val { get; } 
    
    public MyInt(int val)
    {
        Val = val;
    }

    public IValue Eval() => this;
    public string Stringify() => Val.ToString();
}

The changes here are simple. Eval just returns the object itself, and Stringify converts the backing integer to a string and returns it.

Finally, here is the new MyRational class:

public class MyRational : IValue
{
    public int Numerator { get; }
    public int Denominator { get; }

    public MyRational(int numerator, int denominator)
    {
        Numerator = numerator;
        Denominator = denominator;
    }

    public IValue Eval() => this;

    public string Stringify() => $"{Numerator}/{Denominator}";
}

Nothing fancy here as well – MyRational again returns itself from Eval and has its own string representation.

So far, it’s all quite intuitive. Let’s move to the more exciting part – dealing with the Addition expression.

Evaluate an “Addition” with “Rational” Operands

As you saw in Part #3, adding the Rational type means that the Addition expression should cover all of the four (int/rational, int/rational) possible combinations of the types of the arguments.

In other words, Addition evaluation needs to implement the cells of the following grid:

From a coding perspective, this means we should modify the current implementation that was just summing up two integers:

public class Addition : IExpression
{
    // …

    public int Eval() => _operand1.Eval() + _operand2.Eval();
    
    // …
}

This can’t work anymore.

Now, the Addition operands are of type IValue, and the result from Eval should also be an IValue.

// What should we do here???
public IValue Eval() => throw new NotImplementedException();

I think it’s worth asking yourself how you would solve this and spend some time trying to implement it.

There are several ways to approach the problem. In this post, though, I want to focus on the one that strictly follows the laws of Object-Oriented Programming. This also happens to be the most complex one.

Let’s start exploring some possible implementations.

The Semi-OOP Solution

OOP encourages us to define all the operations in terms of the objects themselves.

So if we want to add two IValue objects together, this will be expressed as one of the objects adding the other one to itself. Therefore, I’ll extend the IValue interface with a new AddValue method:

public interface IValue : IExpression
{
    IValue AddValue(IValue operand);
}

By doing that, the Eval method in Addition would look something like this:

public class Addition : IExpression
{
    // ...

    public IValue Eval() => _operand1.Eval().AddValue(_operand2.Eval());

    // ...
}

That is a reasonable OOP style. _operand1.Eval() would produce an object of type IValue. This object has an implementation for the AddValue() method, so we call it by passing the result from _operand2.Eval()

So far, so good. But here comes the tricky part.

How do we implement AddValue in MyInt and MyRational?

Let’s take, for example, the MyInt class:

public class MyInt : IValue
{
    // ...

   public IValue AddValue(IValue operand)
   {
      // How to add IValue to MyInt?
   }

    // ...
}

The problem here is that operand can be either of type MyInt or MyRational. The implementation greatly depends on that.

If it’s a MyInt, we just take the underlying integer, add it to the one in the current class and return a new MyInt instance with the resulting int.

If it’s a MyRational, we take the numerator and denominator and do the appropriate arithmetics with the current integer. We then wrap the result in a MyRational object and return it.

You see how, either way, we need to know the type of operand. One approach would be just to check for it explicitly. Like so:

public IValue AddValue(IValue operand)
{
    switch (operand)
    {
        case MyInt op: 
            return new MyInt(this.Val + op.Val);
        case MyRational op:
            return new MyRational(this.Val * op.Denominator + op.Numerator, op.Denominator);
        default:
            throw new ArgumentOutOfRangeException(nameof(operand));
    }
}

This style is indeed valid, and it solves our problem. I won’t argue whether it is good or bad from a design perspective. But one thing is sure though – it’s not pure OOP.

These blog series have already shown tons of examples and explanations of how “idiomatic” Object-Oriented Programming looks. One of the basic rules is that it’s inadvisable to check for types explicitly.

Again, I’m not making a statement of what is right or wrong. Remember, my goal is to give you the full OOP solution so you could make a comparison between the different styles for yourself.

In OOP, you have a powerful tool to switch behavior based on the types of objects at runtime. Of course, I refer to Polymorphism/Dynamic Dispatch/Late Binding. This is what I’ll utilize to the fullest for a complete OOP solution.

The “Pure” OOP Solution – Double Dispatch

Let’s bring the discussion back to the polymorphic AddValue call in the Addition class:

public IValue Eval() => _operand1.Eval().AddValue(_operand2.Eval());

Based on the underlying IValue object, we will end up executing the AddValue version of either MyInt or MyRational class.

This is the first dispatch.

Now, with the agreement that checking for types is disallowed in OOP, let’s get back to the AddValue method and think of an alternative approach:

public class MyInt : IValue
{
    // ...

   public IValue AddValue(IValue operand)
   {
        // How to implement that in pure OOP?
   }
    // ...
}

I’ll again work with the MyInt class, but everything will apply to MyRational as well.

Here is an idea.

In the context of the MyInt class, we don’t know the type of operand. But we do know the type of the current object(the type of this) – it’s MyInt. Which means, we know the type of one of the Addition operands.

Somehow, from here, we need to dispatch the execution to a context where the types of both operands are known.

This is easier to absorb with an example. So I’ll give you with the solution first, and provide a detailed description afterward.

First, let’s add two more methods to the IValue interface – AddInt(MyInt) and AddRational(MyRational):

public interface IValue : IExpression
{
    IValue AddValue(IValue operand);
    IValue AddInt(MyInt operand);
    IValue AddRational(MyRational operand);
}

These methods indicate that each value type can add any other value type to itself. Both MyInt and MyRational classes should provide an implementation.

Here’s is the MyInt version:

public class MyInt : IValue
{
    // ...    

    public IValue AddInt(MyInt operand) => new MyInt(Val + operand.Val);

    public IValue AddRational(MyRational operand)
        => new MyRational(operand.Numerator + operand.Denominator * Val, operand.Denominator);

   public IValue AddValue(IValue operand)
   {
        // How to implement that in pure OOP?
   }

    // ...    
}

At this point, we have all the ingredients to solve the puzzle:

  1. We know that the type of this is MyInt.
  2. We still don’t know the type of operand, but we do know that it can add a MyInt instance to itself.

Now, we just call the AddInt method on operand and pass this to it:

public class MyInt : IValue
{
    // ...

    public IValue AddValue(IValue operand) => operand.AddInt(this);

    // ...
}

Based on the exact type of operand, we’ll execute the AddInt implementation either in MyInt or MyRational.

This is the second dispatch.

In summary, we used two polymorphic calls on the IValue interface – first AddValue and then AddInt. Hence the name of the technique – Double Dispatch.

Note that we didn’t have to check for types anywhere. Instead, we solved the issue with pure OOP idioms.

This is the punchline of the discussion. We ended up with a working OOP solution from first principles. However, I’d guess that if you’re not a quite experienced OOP practitioner, you may be scratching your head at the moment trying to figure out this little “piece of magic” we went through.

The implementation is indeed fairly tricky. At least compared to the FP solution you’ve seen in the previous article. Let’s make some final comparisons in the next section.

Comparison with the FP Implementation

I wouldn’t classify the Double Dispatch technique as intuitive or easy to comprehend. It’s an excellent example of how you can add unnecessary complexity to your codebase just by blindly following some “idiomatic” OOP patterns(tricks).

In such cases, I’d argue that you should just abandon OOP or any other paradigm that doesn’t fit the specific scenario. Use whatever makes the most sense and keeps your code clean and maintainable.

Take another look at the Functional implementation from the previous post:

let rec addValues(op1, op2) =
    match (op1, op2) with
    | (MyInt i1, MyInt i2) -> MyInt(i1 + i2)
    | (MyInt i, MyRational(num, den)) -> MyRational(i*den + num, den)
    | (MyRational(_, _), MyInt _) -> addValues(op2, op1)
    | (MyRational(num1, den1), MyRational(num2, den2)) -> MyRational(num1*den2 + num2*den1, den1*den2)

Even the most radical OOP supporters would admit that this piece of code is a lot cleaner and elegant. And it’s not about any F# superpowers. You can achieve very similar implementation with pattern matching in C#.

The important takeaway is that, unfortunately, there aren’t any hard rules on how exactly to implement any non-trivial piece of software. It’s all a matter of context.

That’s what makes our profession so challenging and rewarding!

Summary

In this article, you’ve explored the sophisticated Double Dispatch technique over a concrete example. You also contrasted that with the FP solution, which came out to be a lot easier to comprehend.

By now, you should be convinced that there isn’t a universally ”correct” programming style. It all depends on the use case. Seasoned developers know the strengths and weaknesses of different paradigms. This knowledge is required to choose the “right tool for the right job” and end up with an elegant and maintainable system.

As some of you may have realized, IValue implements a well known OOP pattern called the Visitor. This design pattern and its’ applications are what I’ll review in detail in the next post.

Stay tuned and thanks for reading!

Resources

  1. Programming Languages, Part A
  2. Programming Languages, Part B
  3. Programming Languages, Part C
  4. Agile Principles, Patterns, and Practices in C#
  5. Clean Code
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dmitri
Dmitri
3 years ago

You should avoid the term ‘dynamic dispatch’ as you are using it in your post because, at least in the .NET world, ‘dynamic dispatch’ refers to dynamically dispatching on a type at runtime (i.e., when you have a dynamically-typed variable). Dynamic dispatch is indeed an alternative to the double-dispatch complication that allows you to call the right visitor overload based on the argument passed into the visitor method, something that’s impossible with static dispatch.

Site Footer

Subscribe To My Newsletter

Email address