Skip to Content
Course content

21: Interfaces and Default Interface Methods

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've sat through countless code reviews where developers treat interfaces as nothing more than a strict "to-do list" for a class. They assume that if a method is in an interface, the class must write the logic for it, or the code won't compile. For years, that was essentially true. But if you're still thinking that way, you're missing one of the most powerful evolution tools in modern C#.

The "Interfaces are only blueprints" myth

The misconception is simple: "Interfaces define what a class can do, but they can never define how it does it."

If we believed this, adding a new method to an interface used in a large project would be a nightmare. You'd add the method to the interface, and suddenly, fifty different classes across your entire solution would break because they don't implement that new method. You'd spend your entire afternoon writing boilerplate code just to get the project to compile again.

// The "Old Way" headache
public interface ILogger {
    void Log(string message);
    // If I add 'void LogError(string message)' here, 
    // every single class implementing ILogger just broke.
}

This is where Default Interface Methods (DIMs) come in. They allow you to provide a "fallback" implementation directly within the interface. It's not a class, and it's not an abstract class; it's an interface that actually has a brain.

Adding behavior without breaking your API

Imagine we have a logging system. Most of our loggers just need a basic Log method. But we've decided that every logger should also be able to log a warning. Instead of forcing every single logger class to implement LogWarning, we can provide a default implementation that simply leverages the existing Log method.

public interface ILogger {
    void Log(string message);

    // This is a Default Interface Method
    void LogWarning(string message) {
        Log($"[WARNING]: {message}");
    }
}

public class ConsoleLogger : ILogger {
    public void Log(string message) {
        Console.WriteLine(message);
    }
    // Notice: I didn't implement LogWarning, but I can still use it!
}

Now, if a specific logger—say, a CloudLogger—needs a very special way of handling warnings (like sending an email), it can still override that default. If it doesn't, it just gets the standard behavior for free. This is a lifesaver when you're maintaining a library used by other teams; you can evolve your interfaces without breaking their code.

The catch: Interface vs. Class references

Here is where people usually get tripped up. Default methods are not inherited by the class in the way a base class method is. They are implemented explicitly by default.

If you try to call LogWarning on a variable typed as ConsoleLogger, the compiler will scream at you. Why? Because ConsoleLogger didn't actually define that method; the interface did. To access the default implementation, you have to treat the object as the interface.

ConsoleLogger myLogger = new ConsoleLogger();
// myLogger.LogWarning("Careful!"); // This will fail to compile!

ILogger interfaceLogger = myLogger; 
interfaceLogger.LogWarning("Careful!"); // This works perfectly.

I'll be honest: this design choice by the C# team feels a bit clunky at first. But it's there to prevent "diamond inheritance" problems and to keep the class's own public API clean. Just remember: if the logic lives in the interface default, you need to be talking to the interface.




📋 Practical Task

Implementing a Smart Payment Gateway

You are building a payment processing system. You have an interface IPaymentProcessor that requires a ProcessPayment(decimal amount) method. However, you want to provide a default RefundPayment(decimal amount) method that simply logs a message saying a refund is being requested, as most payment providers handle refunds through a separate, complex API that you'll implement later.

Your Task:

  • Create the IPaymentProcessor interface with the ProcessPayment method and a default implementation of RefundPayment.
  • Create two classes: StripeProcessor and PayPalProcessor. StripeProcessor should use the default refund logic, but PayPalProcessor should provide its own custom RefundPayment logic (e.g., printing "PayPal: Processing refund via PayPal API").
  • In your main program, instantiate both processors.
  • Call ProcessPayment on both.
  • Call RefundPayment on both by casting them to the IPaymentProcessor interface.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.