Search code examples
c#dependency-injectionsolid-principlesc#-8.0default-interface-member

C# 8 Default implementation and Dependency Inversion


With C#8 Microsoft introduced Default Implementation for interface methods. It's still a fairly new feature and there seem to be many concerned bloggers writing about this.

What I'm wondering is if Default Implementation has the potential to be a helpful tool for Dependency Inversion and DI or if it promotes a bad programming style? Does it break any of the well known principles like SOLID?


Solution

  • There's two main design goals of default interface implementation. The more important goes all the way back to guidelines about designing interfaces. In particular, as soon as you publish an interface, it should be set in stone and never ever change. The problem is, this is also a rule that's ignored about... all of the time.

    The first and main utility is that default interface implementation allows you to introduce new members into an interface, without breaking source or binary compatibility with consumers of that (public) interface. This still limits the kind of changes you can do when changing a public interface, but also makes it easier for clients to use the new interface - upgrading is free, and they can start using the new features right away.

    The second design goal is a way to extend classes with traits - this has long been used in game development. The basic idea is that you can add new well-defined behaviours to a class just by having it implement an interface, while also retaining the ability to modify the behaviour in the class itself. This is essentially a relatively weak form of meta-programming.

    Of course, just because those were the design goals doesn't mean that they are the only way you should use default implementations. But if you generalize a bit, you get these two basic uses:

    • Extending interfaces without breaking compatibility and having to version interfaces.
    • Extending classes without having to create a rigid tree hierarchy that you get from class inheritance.

    In fact, you could even argue that this is both simpler, clearer and more powerful than class inheritance. In a way, this is a continuation of the approach started with extension methods - in essence, default interface method implementation is an extension method which is also virtual. The default implementation can only work with the public interface, but an implementing class can also use its own hidden state. It gives C# a limited form of multiple inheritance, without having to deal with how the state of two "parents" is joined together (since interfaces don't have any state).

    Finally, if you're worried about principles like SOLID, let's give that a go:

    • SRP - No real change, a few new options of non-destructively adding new logic in one place. I say this is a slight win for SRP.
    • OCP - You get powerful new ways of extending classes without having to modify the class itself, as long as that class implements an interface where the new feature makes sense. But you can't change the existing classes behaviour this way, so it's still closed for modification. Pretty useful win here.
    • LSP - Not really affected. Some minor victories from allowing you to add new substitutable behaviour to a class just through implementing an interface (with the option to override that behaviour as needed).
    • ISP - This can go both ways. Default implementations may make it easier to have a lot of small interfaces if you have reasonable default implementations. But they can also encourage you to keep modifying existing interfaces instead of adding new ones.
    • DIP - Mostly unaffected. You can more easily add new abstractions if they are reasonable extensions to what you already have, or if a default behaviour can be implemented without relying on state. You can also be tempted to use the default behaviour as contractual, but IMO it's still less of a temptation than in an abstract method (where you also have control over the state).