Understanding Interfaces in .NET Core

Interfaces are a fundamental concept in object-oriented programming, particularly in .NET Core. They define a contract that a class must adhere to, specifying a set of method signatures, properties, events, and indexers that a class implementing the interface must provide.

What is an Interface?

An interface is similar to an abstract class, but it cannot contain implementation details. It's a blueprint that outlines what a type can do, without specifying how it does it. Think of it as a service agreement.

Key Characteristics of Interfaces:

Defining an Interface

You define an interface using the interface keyword. Members within an interface are implicitly public and abstract.


public interface IDrawable
{
    void Draw();
    int GetArea();
}
            

In this example, IDrawable is an interface that defines two members: a Draw method and a GetArea method. Any class that implements IDrawable must provide concrete implementations for both of these members.

Implementing an Interface

To implement an interface in a class, you specify the interface name after the class name, separated by a colon. The class must then provide implementations for all members declared in the interface.


public class Circle : IDrawable
{
    private double radius;

    public Circle(double r)
    {
        radius = r;
    }

    public void Draw()
    {
        Console.WriteLine("Drawing a circle.");
        // Actual drawing logic would go here
    }

    public int GetArea()
    {
        // For simplicity, returning an int, though area is typically double
        return (int)(Math.PI * radius * radius);
    }
}

public class Square : IDrawable
{
    private double side;

    public Square(double s)
    {
        side = s;
    }

    public void Draw()
    {
        Console.WriteLine("Drawing a square.");
        // Actual drawing logic would go here
    }

    public int GetArea()
    {
        return (int)(side * side);
    }
}
            

Here, both Circle and Square classes implement the IDrawable interface, each providing its own specific logic for the Draw and GetArea methods.

Using Interfaces

Interfaces are incredibly useful for achieving polymorphism and decoupling your code. You can write code that operates on the interface type, allowing it to work with any object that implements that interface.

Polymorphic Behavior

Consider a function that accepts an IDrawable object:


public void RenderShape(IDrawable shape)
{
    Console.WriteLine($"Rendering shape with area: {shape.GetArea()}");
    shape.Draw();
}
                

You can then call this function with instances of Circle or Square:


Circle myCircle = new Circle(5);
Square mySquare = new Square(4);

RenderShape(myCircle);
RenderShape(mySquare);
                

This demonstrates how the RenderShape method can work with different types of shapes as long as they implement the IDrawable interface, without needing to know their specific concrete types.

Important: Interfaces provide a way to define a contract without enforcing a particular implementation. This flexibility is crucial for building robust and maintainable applications.

Interface Evolution (C# 8.0 and later)

Since C# 8.0, interfaces can now include default implementations for members. This allows for backwards-compatible evolution of interfaces.


public interface ILogger
{
    void LogMessage(string message);

    // Default implementation
    void LogError(string message)
    {
        LogMessage($"ERROR: {message}");
    }
}

public class ConsoleLogger : ILogger
{
    public void LogMessage(string message)
    {
        Console.WriteLine(message);
    }
    // ConsoleLogger can choose to override LogError or use the default implementation.
}
            

This feature adds another layer of utility to interfaces, enabling them to evolve gracefully.

Benefits of Using Interfaces:

  • Decoupling: Reduces dependencies between different parts of your system.
  • Testability: Makes it easier to write unit tests by allowing you to use mock implementations of interfaces.
  • Polymorphism: Enables treating objects of different types in a uniform way.
  • Extensibility: Allows for new implementations to be added without modifying existing code that uses the interface.