GDKB

Abstract Classes

Intermediate25 Minutes readUpdated July 21, 20263 related concepts

An abstract class is a class that can never be instantiated directly — it exists only to be inherited from, providing shared implementation to its subclasses while explicitly requiring each of them to supply certain methods (`abstract` methods) it deliberately leaves unimplemented. It sits between a concrete base class (fully usable on its own) and an interface (no implementation at all), giving both shared code and an enforced contract at the same time.

On this page

Abstract Classes

Summary

An abstract class is a class that can never be instantiated directly — it exists only to be inherited from, providing shared implementation to its subclasses while explicitly requiring each of them to supply certain methods (abstract methods) it deliberately leaves unimplemented. It sits between a concrete base class (fully usable on its own) and an interface (no implementation at all), giving both shared code and an enforced contract at the same time.

Why This Matters

A concrete base Enemy class with a default Attack() implementation doesn't stop someone from writing new Enemy() directly and getting a generic, purposeless enemy that was never meant to exist on its own — every real enemy was supposed to be a specific subtype. Meanwhile, an interface alone can require every implementer to define Attack(), but can't share any actual code between them, forcing MoveTowardPlayer() or PlayHitReaction() to be duplicated in every single enemy type. An abstract class solves both problems: new Enemy() becomes a compile error, and shared logic lives in one place while each subclass is still forced to supply its own Attack().

Mental Model

An abstract class is an incomplete blueprint stamped "not for construction as-is." A blueprint for "a house" might fully specify the foundation, plumbing, and wiring, while leaving the roof style as a required decision — you cannot build directly from this blueprint; you must first produce a specific variant (a "Colonial" or "Ranch" blueprint) that fills in the missing roof design. The abstract class is the same: it supplies everything that's common and settled, and marks what's still an open decision as something every concrete subclass must resolve.

Core Explanation

  • abstract on a class prevents direct instantiation. new Enemy() is a compile error if Enemy is declared abstract class Enemy, even though the class can still have fields, concrete methods, and constructors — only the direct new is disallowed.
  • abstract methods have no body and must be overridden. public abstract void Attack(); declares the method's signature only; every non-abstract subclass is required to provide an override for it, or the subclass fails to compile.
  • Concrete (non-abstract) members are inherited normally. An abstract class can still define fully-implemented methods, fields, and constructors that every subclass gets for free — abstract classes are not "pure contracts" the way interfaces are; they mix shared implementation with enforced gaps.
  • A subclass that doesn't implement every abstract member must itself be abstract. There's no way to "partially" satisfy an abstract base — either every abstract member is overridden, or the subclass explicitly defers the remaining ones by also being abstract.

Syntax / Theory

C# (Unity)

text
public abstract class Enemy : MonoBehaviour
{
    [SerializeField] protected int maxHealth = 100;
    protected int currentHealth;

    protected virtual void Awake() => currentHealth = maxHealth; // Shared, concrete.

    public abstract void Attack(); // No body — every subclass must supply one.

    public void TakeDamage(int amount) // Shared, concrete — every subclass gets this free.
    {
        currentHealth -= amount;
        if (currentHealth <= 0) Die();
    }

    protected virtual void Die() => Destroy(gameObject);
}

public class RangedEnemy : Enemy
{
    public override void Attack() => FireProjectile(); // Required — compile error without it.
}
csharp
// new Enemy(); // Compile error: cannot create an instance of the abstract class 'Enemy'.
Enemy e = new RangedEnemy(); // Fine — a concrete subclass satisfies every abstract member.

C++ (Unreal — pure virtual functions)

cpp
class AEnemy : public ACharacter
{
public:
    void TakeDamage(int32 Amount); // Shared, concrete.

    virtual void Attack() PURE_VIRTUAL(AEnemy::Attack, ); // "= 0" equivalent — abstract.
};

C++ has no abstract keyword on the class itself — a class becomes abstract automatically the moment it declares any pure virtual function (virtual void Attack() = 0;), and the compiler then refuses to instantiate it directly, exactly matching C#'s behavior.

Unreal Blueprint

A C++ class marked UCLASS(Abstract) cannot be placed directly in a level or spawned — only its Blueprint subclasses can — mirroring an abstract class's "must be subclassed to be used" rule at the editor level.

Practical Example

A save-data exporter defines shared export scaffolding while requiring each concrete format to supply its own serialization step.

text
public abstract class DataExporter
{
    public void Export(string path) // Shared workflow, same for every format.
    {
        string content = Serialize();
        File.WriteAllText(path, content);
        Debug.Log($"Exported to {path}");
    }

    protected abstract string Serialize(); // Each format decides this differently.
}

public class JsonExporter : DataExporter
{
    protected override string Serialize() => JsonUtility.ToJson(data);
}

public class CsvExporter : DataExporter
{
    protected override string Serialize() => BuildCsvString(data);
}

Export() — the file-writing and logging — is written once; only the part that genuinely differs between formats (Serialize()) is left for each subclass to fill in.

Common Mistakes

  • Reaching for an interface when shared implementation is actually needed too. An interface alone cannot hold field state or concrete method bodies — if multiple implementers need identical logic, not just an identical contract, an abstract class (or an interface plus composition) fits better.
  • Making a class abstract with no abstract members at all. If every method already has a default implementation, marking the class abstract only to prevent direct instantiation is usually a sign a private constructor plus a factory method, or simply a concrete base class, was the better fit.
  • Forgetting that a subclass failing to implement one abstract member must itself be declared abstract. This is enforced by the compiler, but the error message ("does not implement inherited abstract member") can be confusing the first time it's encountered.
  • Overusing abstract classes for things that never actually vary. An abstract method that every current and realistically-foreseeable subclass implements identically isn't earning its abstraction — it adds a required override with no actual behavioral difference to justify it.

Game Development Example

An ability system shares cooldown and resource-cost handling across every ability, while requiring each ability to define what actually happens when cast.

text
public abstract class Ability
{
    public float Cooldown { get; protected set; }
    private float lastCastTime = float.NegativeInfinity;

    public bool CanCast(float currentTime) =>
        currentTime - lastCastTime >= Cooldown;

    public void TryCast(float currentTime)
    {
        if (!CanCast(currentTime)) return;
        Execute();
        lastCastTime = currentTime;
    }

    protected abstract void Execute(); // Every ability defines its own effect.
}

public class Fireball : Ability
{
    public Fireball() => Cooldown = 3f;
    protected override void Execute() => SpawnProjectile("Fireball");
}

Adding a new ability means writing only Execute() — the cooldown-gating logic in TryCast/CanCast is written once and shared by every ability that will ever exist in the game.

Best Practices

  • Make a class abstract only when it has at least one member that genuinely differs per subclass and cannot be given a sensible default.
  • Put every piece of logic that's identical across all subclasses as a concrete member on the abstract class — that's the entire point of choosing an abstract class over an interface.
  • Keep abstract method contracts small and focused — a subclass forced to implement five unrelated abstract methods to satisfy one base class is a sign the base class is doing too much.
  • Consider whether an interface (see [[CON-0011]]), possibly combined with composition, would satisfy the same need without forcing a single inheritance chain.

Exercises

  1. Write an abstract Collectible class with a concrete OnPickedUp() that plays a sound and destroys the object, and an abstract ApplyEffect() that subclasses like HealthPickup and CoinPickup implement differently.
  2. Take the DataExporter example and add an XmlExporter subclass, confirming that Export() requires no changes at all to support the new format.
  3. You're designing a UIWidget base for a UI system with 30+ widget types, most of which share layout logic but differ significantly in rendering. Decide between an abstract base class and an interface plus a separate shared layout helper class, and justify the trade-off.
  4. Research the difference between an abstract class and a C# sealed class, and explain why a class can never be both abstract and sealed at the same time.
  • Inheritance — abstract classes are a specialized form of inheritance where the base class deliberately leaves some behavior unimplemented; understanding ordinary inheritance is the necessary foundation.
  • Interfaces — the closest alternative to an abstract class when no shared implementation is needed, only a shared contract; choosing between them is a recurring design decision.
  • Method Overriding — abstract methods are satisfied through the exact same override mechanism as overriding a virtual method, just with a base that provides no default to fall back on.

Further Reading

Related Concepts