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
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 thegreetingsMessage
and theName
property. - And finally a public default interface method
PrintName
, which will print the name along with the prefix. It will callConcatenateGreetingsAndName
private method to get the prefixed name.
- A string
- A class
ImplementDemoInterface
, which is the implementation of theIDemoInterface
interface.- This class just implements the
Name
property
- This class just implements the
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
.
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.
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 ==========
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