iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Abstract Classes

An abstract class can’t be instantiated and can declare members subclasses must implement. Use abstract classes for partial implementations with template-method patterns; reach for interfaces when you only need contracts without shared behaviour.

abstract, virtual, sealed, template

EXAMPLE
// 1) abstract class — partial implementation + required hooks
public abstract class Repository<T>
{
    protected readonly DbContext Db;
    protected Repository(DbContext db) => Db = db;

    public T? GetById(int id) => Db.Set<T>().Find(id);

    public IEnumerable<T> List() => ApplyDefaultFilters(Db.Set<T>()).ToList();

    protected abstract IQueryable<T> ApplyDefaultFilters(IQueryable<T> query);
    //                                ^ subclasses MUST implement
}

public class ActiveUserRepository : Repository<User>
{
    public ActiveUserRepository(DbContext db) : base(db) { }
    protected override IQueryable<User> ApplyDefaultFilters(IQueryable<User> q) =>
        q.Where(u => u.IsActive);
}

// You can't 'new Repository<User>()' — the compiler refuses.
// Subclasses fill in the holes; shared logic lives in the base.

// 2) abstract members — methods AND properties
public abstract class Shape
{
    public abstract double Area { get; }                 // abstract property
    public abstract void Describe();                      // abstract method
}

public class Circle : Shape
{
    public double Radius { get; }
    public Circle(double r) { Radius = r; }
    public override double Area => Math.PI * Radius * Radius;
    public override void Describe() => Console.WriteLine($"Circle r={Radius} A={Area:F2}");
}

// 3) abstract + virtual — mix required and optional overrides
public abstract class Animal
{
    public abstract string Sound();                                // must override
    public virtual string Movement() => "walks";                    // can override
    public string Describe() => $"A {GetType().Name} that {Movement()} and says {Sound()}";
}

public class Dog : Animal
{
    public override string Sound() => "woof";
    // Movement not overridden — uses default 'walks'
}

public class Fish : Animal
{
    public override string Sound()    => "blub";
    public override string Movement() => "swims";
}

// 4) Template method pattern — abstract steps + concrete algorithm
public abstract class ReportBuilder
{
    public string Build()
    {
        var header  = LoadHeader();
        var body    = LoadBody();
        var footer  = LoadFooter();
        return Format(header, body, footer);
    }
    protected abstract string LoadBody();          // subclasses customise the body
    protected virtual  string LoadHeader() => "Report";
    protected virtual  string LoadFooter() => "-- end --";
    protected virtual  string Format(string h, string b, string f) => $"{h}\n{b}\n{f}";
}

public class WeeklyReport : ReportBuilder
{
    protected override string LoadBody() => "weekly stats here";
}

// 5) abstract class vs interface — when to use which
// abstract class wins when:
//   • Sharing concrete fields, methods, or constructors
//   • Common base behaviour with extension points (template method)
//   • Need protected members (interfaces are all public)
//
// interface wins when:
//   • Pure contract; many unrelated types implement it
//   • Multiple inheritance (a class can implement many interfaces, extend one class)
//   • Mix-in of behaviour via default interface methods (C# 8+)
//
// Modern guidance: prefer interfaces + composition; reach for abstract class for genuine 'is-a' families with shared state.

// 6) sealed class + abstract base — closed family
public abstract record Event
{
    public DateTimeOffset OccurredAt { get; init; } = DateTimeOffset.UtcNow;
}
public sealed record OrderPlaced(string OrderId, long TotalCents) : Event;
public sealed record OrderCancelled(string OrderId, string Reason) : Event;
public sealed record OrderShipped(string OrderId, string Carrier, string TrackingNumber) : Event;

static string Describe(Event e) => e switch
{
    OrderPlaced p     => $"placed {p.OrderId} (${p.TotalCents/100m:F2})",
    OrderCancelled c  => $"cancelled {c.OrderId}: {c.Reason}",
    OrderShipped s    => $"shipped {s.OrderId} via {s.Carrier} #{s.TrackingNumber}",
};
// Compiler-verified exhaustiveness when Event is abstract + every concrete subtype is sealed.

// 7) Constructors — abstract classes CAN have them
public abstract class HttpClientBase
{
    private readonly HttpClient _client;
    protected HttpClientBase(HttpClient client) => _client = client;

    protected Task<HttpResponseMessage> GetAsync(string url) => _client.GetAsync(url);
}

public class GitHubApi : HttpClientBase
{
    public GitHubApi(HttpClient client) : base(client) { }
    public Task<HttpResponseMessage> Repos() => GetAsync("https://api.github.com/user/repos");
}

// 8) sealed override — prevent further override
public class Specific : Repository<User>
{
    public Specific(DbContext db) : base(db) { }
    protected sealed override IQueryable<User> ApplyDefaultFilters(IQueryable<User> q) => q;
    //                ^ no subclass of Specific can re-override this method
}

// 9) abstract class with extension points + DI
public abstract class JobBase
{
    private readonly ILogger _log;
    protected JobBase(ILogger log) => _log = log;

    public async Task RunAsync(CancellationToken ct)
    {
        _log.LogInformation("start");
        try { await ExecuteAsync(ct); }
        catch (Exception e) { _log.LogError(e, "job failed"); throw; }
        _log.LogInformation("done");
    }

    protected abstract Task ExecuteAsync(CancellationToken ct);
}

public class CleanupJob : JobBase
{
    public CleanupJob(ILogger<CleanupJob> log) : base(log) { }
    protected override Task ExecuteAsync(CancellationToken ct) => /* cleanup */ Task.CompletedTask;
}

// 10) Common bugs
// • Forgetting 'override' on a method intended to override — compiler errors out (good)
// • Calling an abstract method from the base constructor — runs on a half-built object
// • Adding new abstract members later breaks every subclass — provide a default or use virtual
// • abstract class with too many concrete helpers — extract to a service class
// • abstract class that has no abstract members — just use a non-abstract base + sealed leaves
// • Mixing abstract base + interface with same method — disambiguate explicitly
// • Allowing public state on the abstract base — leaks invariants to subclasses; keep state protected with controlled mutation

Why it matters

Reach for abstract classes when you have shared concrete behaviour plus required extension points (template-method pattern). Pair with sealed record subtypes for pattern-matching exhaustiveness, keep state protected with controlled mutation, and don’t call virtual or abstract methods from a constructor — subclass code runs before its fields are initialised.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
public abstract class Shape {
    public abstract double Area();
}
public class Circle : Shape {
    public double R { get; }
    public Circle(double r) { R = r; }
    public override double Area() => Math.PI * R * R;
}
Try it Yourself »

Discussion

Loading…