An abstract class is a base class that cannot be instantiated directly — it exists only to be inherited. It can contain both abstract members (which derived classes must implement) and non-abstract members (shared implementation). Abstract classes are the right tool when you have a family of related types that share substantial common logic but differ in one or more specific operations. They are the backbone of the Template Method design pattern — the base class defines the structure of an algorithm, derived classes fill in the details.
Abstract Class — Template Method Pattern
// ── Abstract base — defines the structure, requires derived details ───────
public abstract class NotificationSender
{
// ── Abstract members — MUST be implemented by derived classes ─────────
protected abstract string SenderType { get; }
protected abstract Task<bool> DeliverAsync(string recipient, string subject, string body);
// ── Non-abstract shared logic — available to all derived classes ──────
public async Task<NotificationResult> SendAsync(
string recipient,
string subject,
string body)
{
if (string.IsNullOrWhiteSpace(recipient))
return NotificationResult.Fail("Recipient is required");
Console.WriteLine($"[{SenderType}] Sending to {recipient}...");
bool success = await DeliverAsync(recipient, subject, body);
var result = success
? NotificationResult.Ok()
: NotificationResult.Fail($"Delivery failed via {SenderType}");
Console.WriteLine($"[{SenderType}] Result: {result.Message}");
return result;
}
// Non-abstract virtual — derived classes CAN override but don't have to
public virtual string FormatSubject(string subject) => $"[{SenderType}] {subject}";
}
// ── Concrete derived class — must implement all abstract members ───────────
public class EmailSender : NotificationSender
{
private readonly ISmtpClient _smtp;
public EmailSender(ISmtpClient smtp) => _smtp = smtp;
protected override string SenderType => "Email"; // abstract property
protected override async Task<bool> DeliverAsync(
string recipient, string subject, string body)
{
await _smtp.SendAsync(recipient, subject, body);
return true;
}
}
// ── Another concrete implementation ────────────────────────────────────────
public class SmsSender : NotificationSender
{
private readonly ISmsGateway _sms;
public SmsSender(ISmsGateway sms) => _sms = sms;
protected override string SenderType => "SMS";
protected override async Task<bool> DeliverAsync(
string recipient, string subject, string body)
{
// SMS: combine subject and body, truncate to 160 chars
string message = $"{subject}: {body}";
if (message.Length > 160) message = message[..157] + "...";
await _sms.SendAsync(recipient, message);
return true;
}
// Override the optional virtual method
public override string FormatSubject(string subject) => subject.ToUpper();
}
// var emailSender = new NotificationSender(); // compile error — abstract!
var email = new EmailSender(smtp);
var result = await email.SendAsync("alice@example.com", "Welcome", "Thanks for joining!");
: base(). This lets abstract classes enforce invariants at the base level — every concrete subclass that calls the abstract class’s constructor will be validated by it. This is the correct place to put validation logic that applies to every type in the hierarchy.Abstract vs Non-Abstract Members
| Member Type | Can Be Abstract? | Derived Class Must Implement? |
|---|---|---|
| Method | Yes | Yes — must override |
| Property | Yes | Yes — must override |
| Field | No | N/A |
| Constructor | No | N/A — called via base() |
| Static member | No | N/A |
| Event | Yes | Yes — must override |
Common Mistakes
Mistake 1 — Trying to instantiate an abstract class
❌ Wrong — compile error:
var sender = new NotificationSender(); // Cannot create instance of abstract class!
✅ Correct — instantiate a concrete derived class:
NotificationSender sender = new EmailSender(smtp); // ✓ derived type stored in base reference
Mistake 2 — Not implementing all abstract members in a derived class
❌ Wrong — compile error if any abstract member is not overridden:
public class PushSender : NotificationSender
{
// forgot to implement abstract members — compile error!
}
✅ Correct — implement every abstract member, or mark the derived class abstract too.