4

Default interface methods of C# 8 and extension methods have in common that you can "add" a method with implementation to an interface.

"Add" being in quotes here because for extension methods that's not quite precise. Extension methods only associate a method with a type, which makes autocomplete suggest it when you are using a type that has extension methods.

In a framework/library, we have several public APIs where we have an interface and accompanying extension methods within the same assembly and namespace, for example something like this:

namespace SomeFramework.DataTree;

public interface ITreeNode
{
    IEnumerable<TreeNode> Children { get; }
}

public static TreeNodeExtensions
{
    public static IEnumerable<ITreeNode> GetChildrenRecursive(this ITreeNode treeNode)
    {
        // traverse the children recursively using ITreeNode.Children
    }
}

But in modern .NET (6 or later) is there any reason to favor extension methods over default interface methods in a public API of a library or framework when the extension methods are in the same assembly and namespace as the extended interface?

With default interface methods the above example would look like this:

namespace SomeFramework.DataTree;

public interface ITreeNode
{
    IEnumerable<TreeNode> Children { get; }

    IEnumerable<ITreeNode> ChildrenRecursive
    {
        get
        {
            // traverse the children recursively using ITreeNode.Children
        }
    }
}

So far I can mainly see disadvantages of using extension methods in the above scenario:

  1. There still can be cases where the extension methods will not show up in autocompletion because the namespace of the extended interface has not been added (using SomeFramework.DataTree; is missing). That can happen, even if you currently have access to an instance of the interface.
  2. It is not possible to override or mock the extension method for example in a unit test. If you use default interface methods this is possible.
  3. There are no extension properties.

But also can I think of some reason to not use default interface implementations in the above scenario, but I feel they are somewhat esoteric. Or are they not?

  1. A lot of additional methods that are more like helper methods may clutter the interface and makes the actual abstraction less apparent.
  2. Default interface methods have the downside that they are invisible if you do not explicitly cast to that interface, as demonstrated here.
1
  • 4
    "It is not possible to override or mock the extension method for example in a unit test" -- given the description of the extension method, it does not sound like something that makes sense to mock for a unit test (for the same reason you would not want to mock LINQ operators). Mocking sounds like it would distil your unit tests down to merely testing the structure of the code rather than writing complete unit tests which actually test real behaviour; so in this case it seems that that not using a mock would be an overall positive for testing and maintainability. Commented Nov 23, 2022 at 10:21

1 Answer 1

5

The default interface methods have the advantage to enrich the interface while avoiding to reimplement the same code for all the classes that implement the interface.

However, they bear the danger of breaking separation of concerns, and bundle within the interface something that should be in a separate abstraction.

Your example of tree traversal is a very good one in this regard, because there are many ways to traverse trees: from left to right, from right to left, depth first vs breath first, recursively or not. And all these traversals are not bound to the trees. So in this specific case I’d advise not to clutter the interface with graph traversal concerns.

4
  • This makes perfect sense in general. I suspect this goes into the direction of my initial argument 1. about DIMs making the abstraction less apparent. The hard part is to identify whether something should be a separate abstraction or not. In the above example we could also provide a default recursive traversal strategy as a DIM that implementers can override. We could also add an enum parameter with all possible traversal strategies. What heuristics are there that could help us decide whether it is better suited as an extension method (separate abstraction)?
    – bitbonk
    Commented Nov 24, 2022 at 13:14
  • 1
    @bitbonk It is indeed sometimes hard to find the optimal boundary. But looking at the alternatives proposed: 1) adding a default traversal account would infringe Interface Segregation Principle on the interface itself: if later you decide to streamline the interface, you’d need to go across all the code using the interface to see if it doesn’t need the default method, just because you added a method which was not strictly necessary. 2) havin an enum with all possible variants would break the Open Closed Principle, since you’d need to modify the default code everytime you add a new traversal.
    – Christophe
    Commented Nov 24, 2022 at 20:35
  • 1
    The argument will more be about balance: if it’s a tree interface that you don’t expect to reuse in other project, and if in your project you only need a simple default traversal, and you need it in many places, you could consider the DIM as a pragmatic solution.
    – Christophe
    Commented Nov 24, 2022 at 20:39
  • Since in this concrete context we are talking about a public APIs, SOLID practices like ISP and OCP should really be taken to heart, because they help provide APIs that can both be flexible/extensible and stable.
    – bitbonk
    Commented Jan 5, 2023 at 15:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.