C# 8.0 Default Interface Implementations: Virtual Extension Methods Perspective
Interfaces biography
Аs we already know, interfaces are programming language constructs that empower the model of creating loosely-coupled software applications. We recognize the interfaces as contracts between the classes, which in fact means they are providing building of “plug & play” maintainable, testable and extensible architectures. Taking into consideration the analogy of fingerprint we can agree that these language constructs don`t enable or provide any kind of multiple inheritance at all. There is still a big misunderstanding for some developers recognizing this concept as a base of multiple inheritance. It is, in fact, a programming myth and fundamental misconception. Interfaces don`t include implementations, so there is no code logic to be inherited from them.
But, looking through the context of C# 8.0 language version, default interface implementations are on the way. Read on to find out what is the key takeaway and other practical explanations.
Common Real-Life Usage
The current most common usage of interfaces is providing polymorphic behavior defining one of the conceptual pillars of Object-oriented paradigm, polymorphism. We can also recognize the benefit incorporated within the well-known architecture designs and principles of developing modern software applications. Think about Domain centric architectures and the separation of concerns as a result of using interfaces. So, it is easy to understand we are talking about interfaces consist of only method signatures. Let`s simplify the story and isolate one point.
Taking one single interface from complex application architecture means we have one or group of just defined methods. There is no implementation and no logic at all. So, following the rules of OOP, we have a lot of concrete classes that are implementing this interface. Implementation is self-descriptive; we are providing implementation following the design of the contract. Moreover, this part of the architecture or maybe it is better to say, this layer of the application, can be and is used by other applications or libraries. And everything is working properly, everything is up and running. Great! But what happens when in some later stage we need to add one more method signature within the interface? Not a big deal, we add the method name and parameters, but now the compiler says we need implementation in each of the classes implementing the interfaces. Not a big deal again, we are implementing the newly added method, but what happens when we have a class where we don`t need this method implementation? What if other developers are implementing the interfaces, but there is no reason to write additional logic for this additional method. This is a sign for a breaking change, and it is not good programming news.
C# 8.0 Default Implementations in Action
C# 8.0 is addressing this case introducing the concept of default interface implementations. Wait, what? Method implementation within the interface, that`s right. Using Microsoft Visual Studio Community 2019 IDE version 16.1.2 and .NET Core 3.0 SDK with C# 8.0 version we will go through some use cases that will show the power of the feature which in fact is changing our current coding routine. For the sake of simplicity, we will use Logger case scenario.
First, let`s think of how we can design log activity or more precisely isolate group of activities in our application. In general, we can design logging mechanism for tracking the flow on the console and in the database. At the same time, we are taking care of flexibility, the concept and future places of using dependency injection, testability, etc. In this context, we will write down two different interfaces defined as following:
Of course, there are other approaches in designing such an implementation, for example, modeling with the creation of just one ILogger interface. But let`s focus on the point with this simplified segmentation, meaning that we are going to avoid the additional different methods definitions that are modeling the specific way of logging. Let`s create Logger class that implements the interfaces and provide the business logic respectively.
So far, so good. Have in mind that again, for the seek of simplicity, Db logging is implemented on the console. We are not going deeper into the logic purpose now. The point is something else.
Now, what if we want to extend IConsoleLogger with an additional method for logging INFO message with a specified color. But not all users want to implement additional logic just for the color. We just decided that we don`t need the explicit implementation even in the already created Logger class as well. C# 8.0 makes this possible. Using the benefits of default interface implementations, we are now allowed to do the following:
Wait there, code body directly in the interface? Yes, exactly. So now, there is no compile time error if Logger class is not explicitly implemented newly added changes. There are no compile errors in all classes that implement this interface. We can implement the logic for methods with default implementations, but we are not ‘forced’ to do it. This means there will be no breaking changes in the code hierarchy.
One important thing here is that if we decide not to implement some default interface implementation then this method is not accessible through object instance from the concrete class.
So, ConsoleLogInfo method is not a suggestion offered by VS intellisense. The only way of accessing the default interface code block implementation is converting a class object into interface type.
Also, we can use default logic, if for example, we have a method with input parameter defined as interface type. A simple analogy can be presented as follows:
Now, let`s make things more exciting. Imagine that somehow, we define new LogTimeStamp method within both interfaces. Besides the message, this method will log the current date and time as well. Even though the signatures are the same, we have a clear scenario because the methods are isolated in different interfaces scope.
Addressing Multiple Inheritance and Diamond Problem Challenges
But we can agree that this simple approach can evolve with the need of implementing another interface which will inherit the methods from the existing ones. And, I suppose that some of you are already thinking about the possibility of defining and implementing a method with the same signature as already present in IConsoleLogger and IDbLogger.
For example, IMonitoringLogger defined as following:
Let`s continue with the code implementation and create another MonitoringLogger class implementing this interface.
Now, if we try to create MonitoringLogger object and contextually cast to IMonitoringLogger we might end up in a ‘potentially problematic’ scenario wondering if we can call LogTimeStamp method.
Even in such a case, we will face with compiler defensive mechanism giving error which prevents this ambiguous flow.
Another interesting way of thinking can be identified through the explanation of well-known Diamond Problem or Deadly Diamond of Death. This is closely related to the concept of multiple inheritance which in fact is not supported in C# programming language. So, let`s create another ISimpleLogger interface and design default logging functionality.
Again, we are dealing with compile time error which means the compiler will prevent unpredictable consequences due to a potential diamond problem. We can of course address this by simply providing implementation for Log method within IMonitoringLogger interface. In this case, we don`t face compile time error anymore. C# 8 forces the most specific override rule.
Final words
In general, the main reason for providing this feature is, in fact, the possibility of interface extension without breaking existing legacy code logic and structure. Default implementations are identified as a new ‘era’ of designing our existing solutions because they provide another way of injecting methods and logic to our code bases. But, at the same time, this feature is recognized as ‘hot topic’ and point of discussion. There are different opinions and explanations.
Compared to the concept of abstract classes, here we have the possibility of implementing logic in multiple interfaces that will be implemented by some concrete class. In the case of abstract class, we are limited only to that class due to the rule of no support for multiple inheritance in C#. So, I think we have a choice and flexibility here. I personally see the perspective in designing and extending existing functionality implemented above complex legacy code base where there are a lot of references and complementary relations. One standard practice is to split interfaces in smaller chunks of method signatures. But, after that, the most common scenario is enabling some of the methods signatures to be inherited in another interface. So, using this virtual extension perspective of the language can address one part of the design challenge.
There is another perspective as well. Does the IL support this feature even before version 8.0? Practical experiment and IL point of view can be found in Gunnar Peipman`s blog post.
The things are getting clear following the official Microsoft documentation tutorial.
In the end, what is the best way of understanding the purpose of this feature? Is it a new powerful way of extending existing functional application modules or maybe another candidate for additional future evolution?
Taking into consideration all news, ideas and plans presented on the last Microsoft Build Conference we should agree that a lot of interesting and exciting C# perspectives are coming.
------------------------
Thank you for reading my article.
I look forward to your thoughts below. Feel free to share your opinion related to the topic.
Enterprise, Integrations Architect | Designing Azure Solutions | API Development | Microservices and Event-Driven Systems | .NET Core Ecosystem Expert | Full Stack | Ex-Honeywell | System Design | Author | Mentor
5yWell explained