Last September, Microsoft released a bunch of new features with C# 8.0. One of the major features that they introduced is Default interface methods. In this post, we'll explain what they are, show a few examples and provide guidance to avoid common pitfalls.
Interfaces in C# used to be definitions or contracts without any behavior. This assumption is no longer true. Now, with default interface methods, you can define a default body for any interface methods. When you do so, they mostly behave like virtual methods in an abstract class.
If they behave like virtual methods on an abstract class, why not simply use an abstract class? There is one big difference that justifies their existence:
In C#, you can only inherit from one base class, but you can implement as many interfaces as you want.
That subtle point makes all the difference when it comes to code reuse, a.k.a. the Don't Repeat Yourself (DRY) principle. That new feature will enable many scenarios that were previously difficult or impossible to achieve.
e.g.
Without default interface methods
internal interface ILogger
{
void WriteCore(LogLevel level, string message);
void WriteInformation(string message);
void WriteWarning(string message);
void WriteError(string message);
}
internal class ConsoleLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
Console.WriteLine($"{level}: {message}");
}
public void WriteInformation(string message)
{
this.WriteCore(LogLevel.Information, message);
}
public void WriteWarning(string message)
{
this.WriteCore(LogLevel.Warning, message);
}
public void WriteError(string message)
{
this.WriteCore(LogLevel.Error, message);
}
}
internal class TraceLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
switch (level)
{
case LogLevel.Information:
Trace.TraceInformation(message);
break;
case LogLevel.Warning:
Trace.TraceWarning(message);
break;
case LogLevel.Error:
Trace.TraceError(message);
break;
}
}
public void WriteInformation(string message)
{
this.WriteCore(LogLevel.Information, message);
}
public void WriteWarning(string message)
{
this.WriteCore(LogLevel.Warning, message);
}
public void WriteError(string message)
{
this.WriteCore(LogLevel.Error, message);
}
}
For every implementation of `ILogger` we have to repeat this same code block:
public void WriteInformation(string message)
{
this.WriteCore(LogLevel.Information, message);
}
public void WriteWarning(string message)
{
this.WriteCore(LogLevel.Warning, message);
}
public void WriteError(string message)
{
this.WriteCore(LogLevel.Error, message);
}
With default interface methods
internal interface ILogger
{
void WriteCore(LogLevel level, string message);
void WriteInformation(string message)
{
this.WriteCore(LogLevel.Information, message);
}
void WriteWarning(string message)
{
this.WriteCore(LogLevel.Warning, message);
}
void WriteError(string message)
{
this.WriteCore(LogLevel.Error, message);
}
}
internal class ConsoleLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
Console.WriteLine($"{level}: {message}");
}
}
internal class TraceLogger : ILogger
{
public void WriteCore(LogLevel level, string message)
{
switch (level)
{
case LogLevel.Information:
Trace.TraceInformation(message);
break;
case LogLevel.Warning:
Trace.TraceWarning(message);
break;
case LogLevel.Error:
Trace.TraceError(message);
break;
}
}
}
Haaaaa... this is much better. Now the implementations only contain the relevant code.
As much as multiple inheritance is awesome, you also need to be very mindful of the dreaded diamond problem.
It's an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C?
There, you have another great tool to put in your toolbox to keep your code clean and maintainable. This approach is also very non-intrusive and won't conflict with existing code, which means it will be easy to slowly adopt it. If you want to hear more on this subject, let me know in the comments.
All code samples are available on github