Interfaces and Abstract Classes

Master the differences between interfaces and abstract classes in C# and learn when to use each for proper abstraction

C#
OOP
Abstraction
Design
20 min read

Overview

Understanding when to use interfaces versus abstract classes is crucial for designing flexible, maintainable systems. The choice impacts testability, extensibility, and code organization.

Core Concepts

Interfaces - Defining Contracts

// Interface with properties, methods, and events
public interface INotificationService
{
    // Property
    bool IsEnabled { get; }

    // Method
    Task SendAsync(string message, string recipient);

    // Event
    event EventHandler<NotificationSentEventArgs> NotificationSent;
}

// Multiple interface implementation
public interface ILoggable
{
    void Log(string message);
}

public interface IDisposable
{
    void Dispose();
}

public class EmailService : INotificationService, ILoggable, IDisposable
{
    public bool IsEnabled { get; private set; } = true;

    public event EventHandler<NotificationSentEventArgs> NotificationSent;

    public async Task SendAsync(string message, string recipient)
    {
        // Send email
        NotificationSent?.Invoke(this, new NotificationSentEventArgs(recipient));
    }

    public void Log(string message) => Console.WriteLine(message);

    public void Dispose() => IsEnabled = false;
}

Abstract Classes - Shared Implementation

public abstract class PaymentProcessor
{
    // Protected field - shared state
    protected readonly ILogger _logger;

    // Constructor - initialize shared dependencies
    protected PaymentProcessor(ILogger logger)
    {
        _logger = logger;
    }

    // Template Method Pattern - defines algorithm structure
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogInformation("Starting payment processing");

        // Shared validation
        ValidateRequest(request);

        // Abstract - derived classes implement
        var result = await ExecutePaymentAsync(request);

        // Shared logging
        _logger.LogInformation("Payment completed: {Status}", result.Status);

        return result;
    }

    // Concrete method - shared implementation
    protected virtual void ValidateRequest(PaymentRequest request)
    {
        if (request.Amount <= 0)
            throw new ArgumentException("Amount must be positive");
    }

    // Abstract method - must be implemented
    protected abstract Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request);
}

public class CreditCardProcessor : PaymentProcessor
{
    public CreditCardProcessor(ILogger logger) : base(logger) { }

    protected override async Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request)
    {
        // Credit card specific implementation
        _logger.LogInformation("Processing credit card payment");
        // ... implementation
        return new PaymentResult { Status = "Success" };
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public PayPalProcessor(ILogger logger) : base(logger) { }

    protected override async Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request)
    {
        // PayPal specific implementation
        _logger.LogInformation("Processing PayPal payment");
        // ... implementation
        return new PaymentResult { Status = "Success" };
    }

    // Override validation for PayPal-specific rules
    protected override void ValidateRequest(PaymentRequest request)
    {
        base.ValidateRequest(request);
        if (string.IsNullOrEmpty(request.PayPalEmail))
            throw new ArgumentException("PayPal email required");
    }
}

Default Interface Methods (C# 8+)

public interface IConfigurable
{
    // Abstract member - must be implemented
    string GetConfiguration(string key);

    // Default implementation - can be overridden
    string GetConfigurationOrDefault(string key, string defaultValue)
    {
        var value = GetConfiguration(key);
        return string.IsNullOrEmpty(value) ? defaultValue : value;
    }

    // Static member (C# 11+)
    static string DefaultConfigPath => "appsettings.json";
}

public class AppConfiguration : IConfigurable
{
    private readonly Dictionary<string, string> _config = new();

    public string GetConfiguration(string key)
    {
        return _config.TryGetValue(key, out var value) ? value : null;
    }

    // Optional: override default implementation
    // public string GetConfigurationOrDefault(string key, string defaultValue) { ... }
}

// Usage
IConfigurable config = new AppConfiguration();
var value = config.GetConfigurationOrDefault("Key", "Default");

Explicit Interface Implementation

public interface IReader
{
    void Read();
}

public interface IWriter
{
    void Write();
}

// Both interfaces might have conflicting members
public interface IFileReader
{
    void Open();
}

public interface INetworkReader
{
    void Open();  // Same name as IFileReader!
}

public class DataProcessor : IFileReader, INetworkReader
{
    // Explicit implementation - resolves naming conflicts
    void IFileReader.Open()
    {
        Console.WriteLine("Opening file");
    }

    void INetworkReader.Open()
    {
        Console.WriteLine("Opening network connection");
    }

    // Implicit implementation - default behavior
    public void Open()
    {
        Console.WriteLine("Opening default");
    }
}

// Usage
var processor = new DataProcessor();
processor.Open();                    // "Opening default"
((IFileReader)processor).Open();     // "Opening file"
((INetworkReader)processor).Open();  // "Opening network connection"

Common Built-in Interfaces

// IDisposable - cleanup resources
public class DatabaseConnection : IDisposable
{
    private bool _disposed;

    public void Dispose()
    {
        if (!_disposed)
        {
            // Cleanup
            _disposed = true;
        }
    }
}

// IEnumerable<T> - enable foreach
public class CustomCollection : IEnumerable<int>
{
    private readonly int[] _items = { 1, 2, 3 };

    public IEnumerator<int> GetEnumerator()
    {
        foreach (var item in _items)
            yield return item;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// IComparable<T> - enable sorting
public class Person : IComparable<Person>
{
    public string Name { get; set; }

    public int CompareTo(Person other)
    {
        return string.Compare(Name, other?.Name, StringComparison.Ordinal);
    }
}

// IEquatable<T> - value equality
public class Point : IEquatable<Point>
{
    public int X { get; set; }
    public int Y { get; set; }

    public bool Equals(Point other)
    {
        if (other is null) return false;
        return X == other.X && Y == other.Y;
    }

    public override bool Equals(object obj) => Equals(obj as Point);
    public override int GetHashCode() => HashCode.Combine(X, Y);
}

Interview Tips

Tip 1: Always mention that interfaces are about "what" (capabilities) while abstract classes are about "what" and partial "how" (shared behavior).

Tip 2: Know that interface is the go-to choice for dependency injection and unit testing because it enables mocking.

Tip 3: Be ready to discuss C# 8 default interface methods and how they change the interface vs abstract class decision.

Common Interview Questions

  1. What is the main difference between an interface and an abstract class?

    • Interface: Contract only, multiple inheritance, no state, members are public. Abstract class: Contract + implementation, single inheritance, can have state and constructors. Use interface for capabilities; abstract class for shared implementation among related classes.
  2. When would you use an interface over an abstract class?

    • Use interfaces when: defining capabilities that unrelated classes can implement, need multiple inheritance, designing for testability/mocking, creating loosely coupled systems. Use abstract classes when: sharing code among closely related classes, need protected members or state.
  3. Can an abstract class have a constructor?

    • Yes. Abstract class constructors initialize shared state and are called by derived class constructors via base(). They ensure proper initialization of the base class portion.
  4. What are default interface methods and why were they added?

    • C# 8 feature allowing interfaces to have method implementations. Added for: library evolution without breaking changes, reducing need for abstract base classes, traits-like composition. Derived classes can override them.
  5. How do you implement multiple interfaces with the same method signature?

    • Use explicit interface implementation: void IInterface1.Method() and void IInterface2.Method(). Access specific implementation by casting to the interface type.
  6. Can you instantiate an interface or abstract class?

    • No direct instantiation. Interfaces are instantiated through implementing classes. Abstract classes are instantiated through derived concrete classes. Both can be used as variable types for polymorphism.
  7. What is the diamond problem and how does C# handle it?

    • Multiple inheritance can cause ambiguity when two parent classes have the same method. C# avoids this by allowing only single class inheritance. Multiple interface inheritance is allowed; conflicts are resolved with explicit implementation.