Why is the default implementation of interfaces useful?

In my last post, I promised to talk about some cases in which, I think, it makes sense to consider the use of default implementations in interfaces. This feature, of course, does not cancel out many existing conventions for writing code, but I found that in some situations, using the default implementation leads to cleaner and more readable code (at least in my opinion).



Expanding Interfaces with Backward Compatibility



The documentation says:

The most common scenario is to safely add methods to an interface already published and used by countless clients.
The problem to be solved is that each class inherited from the interface must provide an implementation for the new method. This is not very difficult when the interface is used only by your own code, but if it is in the public library or used by other commands, then adding a new interface element can lead to a big headache.



Consider an example:



interface ICar { string Make { get; } } public class Avalon : ICar { public string Make => "Toyota"; }
      
      





If I want to add a new GetTopSpeed ​​() method to this interface, I need to add its implementation in Avalon :



 interface ICar { string Make { get; } int GetTopSpeed(); } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; }
      
      





However, if I create a default implementation of the GetTopSpeed ​​() method in ICar , then I will not have to add it to each inherited class.



 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; }
      
      





If necessary, I can still overload the implementation in classes for which the default is not suitable:



 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; }
      
      





It is important to consider that the default method GetTopSpeed ​​() will be available only for variables cast to ICar and will not be available for Avalon if it does not have overload. This means that this technique is most useful if you are working with interfaces (otherwise, your code will be flooded with a lot of casts to interfaces to gain access to the default implementation of the method).



Mixins and traits (or something like that)



Similar language concepts of mixins and traits describe ways to extend the behavior of an object through composition without the need for multiple inheritance.



Wikipedia reports the following about mixins:

Mixin can also be considered as an interface with default methods
Does that sound like that?



But still, even with a default implementation, interfaces in C # are not mixins. The difference is that they can also contain methods without implementation, support inheritance from other interfaces, can be specialized (apparently, referring to the limitations of the templates. - approx. Transl.) And so on. However, if we make an interface that contains only methods with a default implementation, it will, in fact, be a traditional mixin.



Consider the following code, which adds functionality to the object of "movement" and tracking its location (for example, in game dev):



 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // ,         public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; }
      
      





Oh! There is a problem in this code that I did not notice until I started writing this post and tried to compile an example. Interfaces (even those that have a default implementation) cannot store state. Therefore, interfaces do not support automatic properties. From the documentation :

Interfaces cannot store instance state. Although static fields are now allowed on interfaces, instance fields can still not be used. Therefore, you cannot use automatic properties, since they implicitly use hidden fields.
In this C # interfaces are at odds with the concept of mixins (as far as I understand them, mixins can conceptually store state), but we can still achieve the original goal:



 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // A method that changes location // using angle and speed public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; // ,         public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } }
      
      





Thus, we achieved what we wanted by making the Move () method and its implementation available to all classes that implement the IMovable interface. Of course, the class still needs to provide an implementation for the properties, but at least they are declared in the IMovable interface, which allows the default implementation of Move () to work with them and ensures that any class that implements the interface will have the correct state.



As a more complete and practical example, consider a mixin for logging:



 public interface ILogger { public void LogInfo(string message) => LoggerFactory .GetLogger(this.GetType().Name) .LogInfo(message); } public static class LoggerFactory { public static ILogger GetLogger(string name) => new ConsoleLogger(name); } public class ConsoleLogger : ILogger { private readonly string _name; public ConsoleLogger(string name) { _name = name ?? throw new ArgumentNullException(nameof(name)); } public void LogInfo(string message) => Console.WriteLine($"[INFO] {_name}: {message}"); }
      
      





Now in any class I can inherit from the ILogger interface:



 public class Foo : ILogger { public void DoSomething() { ((ILogger)this).LogInfo("Woot!"); } }
      
      





And such a code:



 Foo foo = new Foo(); foo.DoSomething();
      
      





Will output:



 [INFO] Foo: Woot!
      
      





Replacing Extension Methods



The most useful application I have found is replacing a large number of extension methods. Let's get back to a simple logging example:



 public interface ILogger { void Log(string level, string message); }
      
      





Before default implementations appeared in interfaces, as a rule, I would write a lot of extension methods for this interface, so that in an inherited class only one method needs to be implemented, as a result of which users would have access to many extensions:



 public static class ILoggerExtensions { public static void LogInfo(this ILogger logger, string message) => logger.Log("INFO", message); public static void LogInfo(this ILogger logger, int id, string message) => logger.Log("INFO", $"[{id}] message"); public static void LogError(this ILogger logger, string message) => logger.Log("ERROR", message); public static void LogError(this ILogger logger, int id, string message) => logger.Log("ERROR", $"[{id}] {message}"); public static void LogError(this ILogger logger, Exception ex) => logger.Log("ERROR", ex.Message); public static void LogError(this ILogger logger, int id, Exception ex) => logger.Log("ERROR", $"[{id}] {ex.Message}"); }
      
      





This approach works great, but not without flaws. For example, class namespaces with extensions and interfaces do not necessarily match. Plus annoying visual noise in the form of a parameter and a link to a logger instance:



 this ILogger logger logger.Log
      
      





Now I can replace the extensions with default implementations:



 public interface ILogger { void Log(string level, string message); public void LogInfo(string message) => Log("INFO", message); public void LogInfo(int id, string message) => Log("INFO", $"[{id}] message"); public void LogError(string message) => Log("ERROR", message); public void LogError(int id, string message) => Log("ERROR", $"[{id}] {message}"); public void LogError(Exception ex) => Log("ERROR", ex.Message); public void LogError(int id, Exception ex) => Log("ERROR", $"[{id}] {ex.Message}"); }
      
      





I find this implementation cleaner and easier to read (and support).



Using the default implementation also has several more advantages over extension methods:





What confuses me in the code above is that it’s not quite obvious which interface members have a default implementation and which are part of the contract that the inherited class should implement. A comment separating the two blocks might help, but I like the strict clarity of extension methods in this regard.



To solve this problem, I began to declare interfaces that have members with the default implementation as partial (except perhaps very simple ones). Then I put the default implementations in a separate file with a naming convention of the form “ILogger.LogInfoDefaults.cs” , “ILogger.LogErrorDefaults.cs” and so on. If there are few default implementations and there is no need for additional grouping, then I name the file "ILogger.Defaults.cs" .



This separates the members with default implementation from the non-implementable contract, which inherited classes are required to implement. In addition, it allows you to cut very long files. There is also a tricky trick with rendering ASP.NET -style attached files in projects of any format. To do this, add to the project file or in Directory.Build.props :



 <ItemGroup> <ProjectCapability Include="DynamicDependentFile"/> <ProjectCapability Include="DynamicFileNesting"/> </ItemGroup>
      
      





Now you can select “File Nesting” in Solution Explorer and all your .Defaults.cs files will appear as descendants of the “main” interface file.



In conclusion, there are still several situations in which extension methods are preferred:






All Articles