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.
class DigitalOrder : Order, IArchivable // base class first, then interfacesAn 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
// 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 40When 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.
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: 42Checking 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.
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 archivableAn 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.
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