Click here to Skip to main content
15,846,860 members
Articles / Programming Languages / C#

Dynamic Polymorphism: Key Concept to Master OOP

Rate me:
Please Sign up or sign in to vote.
4.95/5 (14 votes)
27 Jun 2023CPOL6 min read 6.4K   21   1
A look at what dynamic polymorphism is and how it is crucial to mastering OOP
The world of object-oriented programming contains many confusing mnemonics such as SOLID or design patterns. In this article, I argue that you can substitute them with understanding what dynamic polymorphism is and how it is applied.

Introduction

The world of object-oriented programming is a bit confusing. It requires mastering a lot of things: SOLID principles, design patterns to name a few. This gives birth to a lot of discussions: are design patterns still relevant, is SOLID intended solely for object-oriented code? It is said that one should prefer composition to inheritance but what is the exact rule of thumb when one should choose one or another?

Since numerous opinions on this matter were voiced I don’t think that mine will be the final one, but nevertheless, in this article, I’ll present the system that helped me in my everyday programming using C#. But before we jump to that, let’s have a look at another question. Consider the code.

C#
public class A
{
    public virtual void Foo()
    {
        Console.WriteLine("A");
    }
}

public class B : A
{
    public override void Foo()
    {
        Console.WriteLine("B");
    }
}

public class C : A
{
    public void Foo()
    {
        Console.WriteLine("C");
    }
}

var b = new B();
var c = new C();
b.Foo();
c.Foo();

Can you tell what the code will output in each case? If you’ve answered correctly that respective output will be “B” and “C”, then why does override keyword matter?

Enter Dynamic Polymorphism

Polymorphism is believed to be one of the pillars of object-oriented programming. But what exactly does it mean? Wikipedia tells us that polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

I don’t expect you to grasp this definition from the first time so let’s have a look at some examples.

C#
string Add(string input1, string input2) => string.Concat(input1, input2);
int Add(int input1, int input2) => input1 + input2;

Above is an example of ad-hoc polymorphism which refers to polymorphic functions that can be applied to arguments of different types, but that behave differently depending on the type of the argument to which they are applied. So why is polymorphism so important for object-oriented code? This snippet does not provide a clear answer to this question. Let’s look through more examples.

C#
class List<T> {
    class Node<T> {
        T elem;
        Node<T> next;
    }
    Node<T> head;
    int length() { ... }
}

This is an example of parametric polymorphism and to be honest, it looks more functional than object-oriented. Let’s have a look at the final example.

C#
interface IDiscountCalculator
{
    decimal CalculateDiscount(Item item);
}

class ThanksgivingDayDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }
}

class RegularCustomerDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }
}

This is an example of dynamic polymorphism which is the term for applying polymorphism at the runtime (mostly via subtyping). And if you’ve tried to memorize all these design patterns before the interview or some SOLID principles, you may notice the shape of something familiar. Let’s see how dynamic polymorphism manifests itself in these concepts.

Dynamic Polymorphism and Design Patterns

Most design patterns (strategy, command, decorator, etc.) rely on injecting the abstract class or the interface and choosing the implementation of it at runtime. Let’s have a look at some class diagrams to make sure it’s the case.

Image 1

Above is the diagram of strategy pattern where Client works with abstraction and its concrete implementation is chosen during the runtime.

And here’s decorator.

Image 2

In this case, wrapper accepts wrappee which is an instance of abstraction and its implementation may vary during the runtime.

Dynamic Polymorphism and SOLID

When asking about SOLID during the interview, the regular answer I hear is “S stands for single responsibility and O stands for uhm…”. On the contrary, I argue that the latter four letters of this acronym are more important since they represent the set of preconditions for your dynamic polymorphism to run smoothly.

For instance, open-closed principle represents a way of thinking in which you tackle every new problem as the subtype for your abstraction. Recall IDiscountCalculator example. Imagine the case when you have to add another discount (say for father’s day). To satisfy open-closed principle, you have to add another subclass FathersDayDiscountCalculator that performs the calculation.

Let’s move on to the Liskov substitution principle. Imagine the situation that breaks it: we have to check whether the user is actually a father and it’s a matching date. So we add the public method which checks whether the user is eligible.

C#
class FathersDayDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }

    public bool IsEligible(User user, DateTime date)
    {
        //omitted
    }
}

Now the calling code will face some complications:

C#
private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
    decimal result = 0;
    foreach (var  discountCalculator in _discountCalculators)
    {
        if (discountCalculator is FathersDayDiscountCalculator)
        {
            var fathersDayDiscountCalculator = 
                discountCalculator as FathersDayDiscountCalculator;
            if (fathersDayDiscountCalculator.IsEligible(user, DateTime.UtcNow))
            {
                result += fathersDayDiscountCalculator.CalculateDiscount(item);
            }
        }
        else
        {
            result += discountCalculator.CalculateDiscount(item);
        }
    }
    return result;
}

Pretty verbose, isn’t it? So in order to satisfy Liskov substitution principle, we have to force all our implementations to exhibit the same public contract provided by the abstraction. Otherwise, it will complicate the application of dynamic polymorphism.

Another thing that complicates the application of dynamic polymorphism is having abstraction too broad. Imagine that we’ve made IsEligible the part of our interface and now all concrete classes implement it. The calling code is greatly simplified.

C#
private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
    decimal result = 0;
    foreach (var  discountCalculator in _discountCalculators)
    {
        result += discountCalculator.CalculateDiscount(item);
    }
    return result;
}

But now imagine (I know the example is a bit contrived but just for the sake of the argument!) that one of the implementations threw NotImplementedException because it doesn’t make sense for this particular type of discount. At this point, you may foresee the problem CalculateDiscountForItem failing with the runtime exception.

That is what interface-segregation principle is about: don’t make abstractions too broad so that concrete types won’t have trouble with implementing them thus complicating your dynamic polymorphism with unnecessary NotImplementedExceptions.

And by this time, you may observe dependency inversion principle in action. In the example above, we deal with the collection of abstractions and have no idea of their runtime types.

Prefer Composition Over Inheritance

I won’t dive a lot into why composition is preferable. There are numerous examples of how inheritance complicates things. But now, when you have a question about what are legit cases for inheritance, here’s an answer for you: when it facilitates dynamic polymorphism.

Virtual and Override

At this point, those of you who didn’t know the answer correctly to the question at the beginning of the article might have a suspicion that it was a tricky question. And indeed while the behavior is similar when we use var keyword, differences start to arise when we apply dynamic polymorphism. For that matter, let’s convert both instances to parent type.

C#
A b = new B();
A c = new C();

Now the output will be “B” and “A” respectively. Hot memorize this? The goal of override keyword is to facilitate dynamic polymorphism. So think of it this way: when we inject abstraction, we expect to work with the concrete type realization and as override facilitates this goal, so implementation of B will be invoked.

Why This Matters?

So now, you know how to memorize all these pesky interview questions. But the most curious of you may be asking: what are the benefits of such a style of programming? Why do we strive to apply dynamic polymorphism in our object-oriented codebases?

Imagine we have two methods somewhere in our codebase.

C#
public string GetCurrencySign(string currencyCode)
{
    return currencyCode switch
    {
        "US" => "$",
        "JP" => "¥",
        _ => throw new ArgumentOutOfRangeException(nameof(currencyCode)),
    };
}

public decimal GetRoundUpAmount(decimal amount, string currencyCode)
{
    return currencyCode switch
    {
        "US" => Math.Floor(amount + 1),
        "JP" => Math.Floor(amount / 100 + 1) * 100,
        _ => throw new ArgumentOutOfRangeException(nameof(currencyCode))
    };
}

Now imagine we have to add another country support. Doesn’t look like a big deal but imagine these two methods hiding in one of those “real-world” codebases with thousands of classes and hundreds of thousands of lines of code. Most likely, you’ll forget all the places where you should add country support. This is exactly what Shotgun surgery code smell is.

How do we fix it? Let’s extract all the information related to country code in a single place.

C#
public interface IPaymentStrategy
{
    string CurrencySign { get; }
    decimal GetRoundUpAmount(decimal amount);
}

Now when we have to add a new country code, we are forced to implement the interface above so we definitely won’t forget anything. We use factory to return instances of IPaymentStrategy.

C#
public string GetCurrencySign(string currencyCode)
{
    var strategy = _strategyFactory.CreateStrategy(currencyCode);
    return strategy.CurrencySign;
}

In the example above, we’ve fixed a code smell by applying dynamic polymorphism. Occasionally, we’ve managed to satisfy some SOLID principles (namely Open-Closed by crafting new functionality with extensions instead of modification) and applying design patterns. Bunch of cool enterprise stuff for your CV by applying just a single OOD principle!

Conclusion

Software engineers, just like most of us, tend to follow a lot of principles without questioning their rationale. When this is done, principles tend to get distorted and diverted from their original goal. So by questioning what was the original goal, we might apply these principles as they were intended to be applied.

In this article, I’ve argued that one of the core principles beyond OOD was the application of dynamic polymorphism and a lot of principles (SOLID, design patterns) are just mnemonics built around it.

History

  • 27th June, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Ukraine Ukraine
Team leader with 8 years of experience in the industry. Applying interest to a various range of topics such as .NET, Go, Typescript and software architecture.

Comments and Discussions

 
PraiseConfusion Killer Pin
Member 1134863710-Jul-23 10:04
Member 1134863710-Jul-23 10:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.