Interface or Abstract Class

Model a kind of thing with an abstract class and a capability with an interface; a type gets one base class and as many interfaces as it needs.

Syntaxclass DigitalOrder : Order, IArchivable // base class first, then interfaces

An abstract class and an interface both describe something a type must supply, so the choice looks arbitrary. It is not. Ask what you are modelling.

A kind of thing: abstract class

An abstract class is a class that cannot be created on its own. It exists to be inherited. Because it is a class, it can hold fields, run a constructor and have private members. It can mix finished methods with abstract ones, which have no body and must be written by each derived class. Use it when several types really are varieties of one thing and share data or setup: an invoice order and a shipped order are both orders.

A capability: interface

An interface describes something a type can do, with no data and no constructor. A type can implement many interfaces, and the types that implement one need share no ancestry at all. Use it when the trait cuts across unrelated types: an order and an audit entry can both be archivable without being related.

Deciding

Two rules settle most cases. If you need stored data or a constructor, only an abstract class will do. If a type needs two of these at once, at most one of them can be the base class. Combining both is normal: an abstract class for what the type is, interfaces for what it can do.

Default interface members, added in C# 8, blur this slightly by letting an interface carry a body. They still cannot hold state, so the rule stands.

Example

Example · csharp
// The capability holds across unrelated types.
IArchivable[] archivables =
{
    new DigitalOrder("pdf-guide", 25m),
    new AuditEntry("login-failed")
};

foreach (IArchivable item in archivables)
{
    Console.WriteLine(item.ArchiveKey());
}

Order order = new DigitalOrder("desk-lamp", 40m);
Console.WriteLine($"{order.Sku} totals {order.Total()}");

// A kind of thing: the base class holds shared data and a constructor.
abstract class Order
{
    protected Order(string sku) => Sku = sku;

    public string Sku { get; }

    // No body, so every order type must write one.
    public abstract decimal Total();
}

// A capability: any type can take it on, whatever it inherits from.
interface IArchivable
{
    string ArchiveKey();
}

class DigitalOrder : Order, IArchivable
{
    private readonly decimal _price;

    public DigitalOrder(string sku, decimal price) : base(sku)
    {
        _price = price;
    }

    public override decimal Total() => _price;

    public string ArchiveKey() => $"order/{Sku}";
}

// Not an Order at all, yet still archivable.
class AuditEntry : IArchivable
{
    public AuditEntry(string name) => Name = name;

    public string Name { get; }

    public string ArchiveKey() => $"audit/{Name}";
}

// Output:
// order/pdf-guide
// audit/login-failed
// desk-lamp totals 40

When to use it

  • Decide whether PaymentMethod should be a base class or an interface before the hierarchy is written.
  • Keep shared validation and constructor logic in one place while each subclass supplies the differing step.
  • Mark a handful of unrelated types as cacheable so one routine can accept all of them.

More examples

Shared setup is the tell for an abstract class

Build is written once and calls Body, which each subclass supplies. That arrangement needs a field, a constructor and a protected member, so an interface could not express it. Reach for an abstract class the moment you write the word field.

Example · csharp
abstract class Report
{
    // A constructor and a field: an interface can have neither.
    protected Report(string title)
    {
        Title = title;
        _lineCount = 0;
    }

    private int _lineCount;

    public string Title { get; }

    protected void CountLine() => _lineCount += 1;

    public int LineCount => _lineCount;

    // Each report fills in only this part.
    protected abstract string Body();

    // Finished behaviour every report shares.
    public string Build()
    {
        CountLine();
        return $"{Title}\n{Body()}";
    }
}

class SalesReport : Report
{
    public SalesReport() : base("Sales")
    {
    }

    protected override string Body() => "total: 42";
}

Console.WriteLine(new SalesReport().Build());

// Output:
// Sales
// total: 42

Checking for a capability at runtime

Because capabilities are optional, code often has to ask whether a given object has one. The is-pattern and the as-plus-null-check do the same job; the pattern form is what you will see in current code.

Example · csharp
object[] things = { new AuditEntry("login-failed"), "a plain string" };

foreach (object thing in things)
{
    // Shorthand: test the type and name the result in one step.
    if (thing is IArchivable archivable)
    {
        Console.WriteLine(archivable.ArchiveKey());
    }
    else
    {
        Console.WriteLine("not archivable");
    }

    // The longhand this replaces, kept for comparison:
    // IArchivable? maybe = thing as IArchivable;
    // if (maybe is not null)
    // {
    //     Console.WriteLine(maybe.ArchiveKey());
    // }
}

interface IArchivable
{
    string ArchiveKey();
}

class AuditEntry : IArchivable
{
    public AuditEntry(string name) => Name = name;

    public string Name { get; }

    public string ArchiveKey() => $"audit/{Name}";
}

// Output:
// audit/login-failed
// not archivable

An abstract class implementing an interface

A base class may satisfy part of an interface and declare the rest abstract, so subclasses fill in only what differs. This is the usual shape of a plug-in point: callers depend on the interface, implementors inherit the convenience base.

Example · csharp
interface IHealthCheck
{
    string Name { get; }

    bool IsHealthy();
}

// The base class keeps the shared part of the contract and leaves the rest open.
abstract class HealthCheckBase : IHealthCheck
{
    protected HealthCheckBase(string name) => Name = name;

    public string Name { get; }

    public abstract bool IsHealthy();
}

class DiskCheck : HealthCheckBase
{
    public DiskCheck() : base("disk")
    {
    }

    public override bool IsHealthy() => true;
}

IHealthCheck check = new DiskCheck();
Console.WriteLine($"{check.Name}: {check.IsHealthy()}");

// Output:
// disk: True

Discussion

  • Be the first to comment on this lesson.