Deep dive into Default Interface methods in C# 8

In this blog, I am going to deep dive into Default Interface Methods in C# 8. This was released along with .Net Core 3.0 in September 2019. I have covered this topic briefly previously in my blog https://dotnetcorecentral.com/blog/c-8-my-top-5-favorite-features-which-i-absolutely-love/. But at that time I was not sure of the potential of this feature. Now, after going through the details of the feature, I am ready to take a deep dive into it.

The main highlights I am going to cover in this blog are:

  • What is Default Interface Methods
  • An example of Default Interface Methods
  • Solution provided for multiple inheritance problem
  • Why I think we don’t need default interface methods
default interface methods

What is Default Interface Methods

By definition, the default interface method is a method you can define (not just declare, but provide implementation as well) inside of an interface, without breaking the classes which implement the interface. To elaborate, now we can have a method with complete implementation inside of an interface. Which was not possible pre-C# 8.

Let’s talk about what are the advantages of doing that. Well, if you want to extend an interface after it’s been released in production without breaking all the clients that implemented this interface, then the default interface method is your answer. By the way, I am going to contradict this statement later in this blog, but lets first continue with the details.

Another important thing to note, though the feature is called default interface methods. In reality, it can be any member, including method, indexer, property or event.

This feature is equivalent to Trait in other Object-Oriented Programming Languages like PHP or Functional Programming Language like Scala. Trait is an Object-Oriented Programming concept.

What all are supported with default interface methods?

  • Default interface members can have public, internal, protected or private modifiers, just like any other class implementation.
  • The default interface members themselves can be either static or non-static.
  • The members can be virtual, allowing any derived interface or class to extend the member.
  • To support default interface methods, the interface now can have static member variables as well. (Note: Only static member variables are allowed, no instance variables are allowed).
  • The default access modifier is public for the default interface members, which is also the default for other members of an interface pre-C# 8.

An example of Default Interface Methods

Below is an example of default interface methods. This is not a real-life example, but a simple one to demonstrate the feature.

using System;

namespace DefaultInterfaceMethods.Demo
{
    public interface IDemoInterface
    {
        string Name { get; set; }

        private static string greetingsMessage = "Hello ";
        
        private string ConcatenateGreetingsAndName()
        {
            return greetingsMessage + Name;
        }

        public void PrintName()
        {
            Console.WriteLine(ConcatenateGreetingsAndName());
        }
    }

    public class ImplementDemoInterface : IDemoInterface
    {
        private string name;

        public string Name { get => name; set => name = value; }
    }
}

In the above example, we have the following:

  • An interface IDemoInterface, which has the following members:
    • A string Name property, which does not have any implementation, and will have the implementation inside of the class which implements the interface.
    • A private static string member variable greetingsMessage, which is contains the greetings message for the name.
    • A private function ConcatenateGreetingsAndName which concatenates the greetingsMessage and the Name property.
    • And finally a public default interface method PrintName, which will print the name along with the prefix. It will call ConcatenateGreetingsAndName private method to get the prefixed name.
  • A class ImplementDemoInterface, which is the implementation of the IDemoInterface interface.
    • This class just implements the Name property

Using the Default Interface Methods

Now let us implement the Main method and use this interface.

namespace DefaultInterfaceMethods.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            var implementDemoInterface = new ImplementDemoInterface();
            implementDemoInterface.Name = "Nirjhar";
            implementDemoInterface.PrintName();
        }
    }
}

In the above implementation, since I have used var for declaring the variable for instance of ImplementDemoInterface class, it will infer var to the type of ImplementDemoInterface. And as a result, I will get an error when I compile the code. And the error message will be saying that the method PrintName is not available for the class ImplementDemoInterface.

This is another important thing to notice, though for most of you it might be very obvious. But it is worthwhile digging into it. Since the default method PrintName is implemented inside of the interface IDemoInterface, it is in a sense not part of the contract defined by it. Hence, unlike the Name property, the PrintMethod is not available to the type ImplementDemoInterface.

compiler error
Error from compiler

Next, I am going to change the implementation inside of the Main method. This time, instead of using var I will use IDemoInterface to declare the instance for type ImplementDemoInterface.

namespace DefaultInterfaceMethods.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            IDemoInterface implementDemoInterface = new ImplementDemoInterface();
            implementDemoInterface.Name = "Nirjhar";
            implementDemoInterface.PrintName();
        }
    }
}

Once I replace var with IDemoInterface, I am able to compile and run the program. And in the output, I can see "Hello Nirjhar" printed in the console.

default interface methods output
The output from the Main method

Few things to remember

The first important thing to remember is that the default interface method implementation has to be a pure function. This means it is either using existing methods/properties of the same interface to do some computation, or it is a simple computational function. Since we cannot introduce an external dependency inside of an interface.

The second important thing to remember is that we cannot access the default method from the class reference. Meaning, when we declare a reference, it needs to be the reference of the interface type which had the default interface method implemented. This is something we observed in the example above. When we declared the reference of the type ImplementDemoInterface using var, we were getting a compilation error. It is because it inferred to the actual type ImplementDemoInterface and not the interface IDemoInterface, which has the implementation of PrintName.

Solution provided for Multiple Inheritance problem

Due to the introduction of the default interface methods in C# 8, indirectly this feature introduces the classic C++ multiple inheritance problem. Since now multiple interfaces can implement a single base interface. And provide their individual implementations to a default interface method. When those derived interfaces are implemented by a single class we get into the classic C++ multiple inheritance problem. This problem is also known as the diamond problem.

To read more about multiple inheritance and diamond problems; please check out the Wikipedia site on multiple inheritances in the following link: https://en.wikipedia.org/wiki/Multiple_inheritance.

Let us examine this problem through an example and post that we will understand how C# 8 compiler solved this problem.

To examine this problem, let me declare a new set of interfaces. And this time, let me make it close to a real-life example, rather than just for a demo.

Let say we have a Financial application that sells financial products. For simplicity, let us consider it only sells Stocks and Bonds, only two product types. When someone searches for a stock and/or bond, they can buy that product by ordering it. For supporting this we can have a base interface called IOrder, and two interfaces IStockOrder and IBondOrder derived from it.

using System;

namespace DefaultInterfaceMethods.Demo
{
    public interface IOrder
    {
        void Buy();
    }

    public interface IStockOrder : IOrder
    {
        void IOrder.Buy()
        {
            Console.WriteLine("Buy stock!");
        }
    }

    public interface IBondOrder : IOrder
    {
        void IOrder.Buy()
        {
            Console.WriteLine("Buy bonds!");
        }
    }
}

As you can see, in the above implementation, both IStockOrder and IBondOrder have their own implementation of the method Buy. Which makes the Buy method inside of IStockOrder and IBondOrder as a default interface method.

Now I will define a class InstrumentOrder for buying financial instruments, and it will implement both IStockOrder and IBondOrder.

namespace DefaultInterfaceMethods.Demo
{
    public class InstrumentOrder : IStockOrder, IBondOrder
    {

    }
}

Compile output of multiple inheritances

When I compile the program after adding the class above, I will see the compilation error. Since both IStockOrder and IBondOrder have a definition of IOrder.Buy, the compiler finds the ambiguity and created a compiler error. This is how C# compiler ensures that we do not have Diamond or multiple inheritance issues as in C++.

1>------ Build started: Project: DefaultInterfaceMethods.Demo, Configuration: Debug Any CPU ------
1>InstrumentOrder.cs(3,36,3,47): error CS8705: Interface member 'IOrder.Buy()' does not have a most specific implementation. Neither 'IStockOrder.IOrder.Buy()', nor 'IBondOrder.IOrder.Buy()' are most specific.
1>Done building project "DefaultInterfaceMethods.Demo.csproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

multiple inheritance error
Compilation error due to multiple inheritances

Why I think we don’t need Default Interface Methods

Now to the final section of the blog, which is my take on this feature. This is going to be controversial, but I am going to give my opinion regardless.

As I mentioned in my blog: https://dotnetcorecentral.com/blog/c-8-my-top-5-favorite-features-which-i-absolutely-love/ I am not sure this is a good idea. In my opinion, this feature will promote a lazy design.

It is an anti-pattern in my opinion in light of SOLID principles. I would rather use composition instead of default interface methods. Meaning, create an interface/class combination to provide the feature which the default method is about to give. And use this interface where the feature is needed. Lets elaborate by taking the first example and modify it to implement composition.

Composition instead of Default Interface Methods

In the first example of this blog, to provide the PrintName functionality to the class ImplementDemoInterface, I created a default interface method on the interface IDemoInterface. And let’s say I did that because other classes use IDemoInterface, so I did not want to break other classes with this new method.

Well, the question here is, if other classes do not need this method than following the concept of the Single Responsibility Principle, this method does not belong to the interface IDemoInterface. Instead, I will create a new interface, INamePrinter and keep the method PrintName inside of this interface. Now, let’s show the code to make it more clear.

using System;

namespace DefaultInterfaceMethods.Demo
{
    public interface IDemoInterface
    {
        string Name { get; set; }
    }

    public interface INamePrinter
    {
        void PrintName();
    }

    public class ImplementDemoInterface : IDemoInterface, INamePrinter
    {
        private string name;
        private readonly string greetingsMessage = "Hello ";

        public string Name { get => name; set => name = value; }

        public void PrintName()
        {
            Console.WriteLine(ConcatenateGreetingsAndName());
        }

        private string ConcatenateGreetingsAndName()
        {
            return greetingsMessage + Name;
        }
    }
}

In the code above, since I need PrintName only to the ImplementDemoInterface class. I will implement the new interface INamePrinter to this class and provide the PrintName function’s implementation. The rest of the classes implementing the IDemoInterface does not care about this method and does not even have to know about it. After this implementation, there is only one change we have to make to the Main method. We will change the declaration of the reference of ImplementDemoInterface from IDemoInterface to var. Once we do that the program will again work as expected and print "Hello Nirjhar".

namespace DefaultInterfaceMethods.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            var implementDemoInterface = new ImplementDemoInterface();
            implementDemoInterface.Name = "Nirjhar";
            implementDemoInterface.PrintName();
        }
    }
}

There are a couple of other ways to solve this problem but for illustrating the point that the default interface methods are an anti-pattern, this example should be good enough.

Conclusion

In my opinion, default interface methods should not be used, until unless there is a compelling reason to do so. In that case, also it should be done during design time and should not be an afterthought.

YouTube video for this session: https://youtu.be/txmh0VhIhgk