GDKB

Composition vs Inheritance

Advanced30 Minutes readUpdated July 21, 20263 related concepts

Inheritance models an "is-a" relationship by having a subclass extend a base class, inheriting its members automatically. Composition models a "has-a" relationship by building an object out of smaller, independent parts, and delegating to them instead of inheriting their behavior. Both are ways to reuse and organize behavior across types — the decision between them is one of the most consequential design choices in an object-oriented codebase, because inheritance chosen too eagerly is far harder to unwind later than composition is.

On this page

Composition vs Inheritance

Summary

Inheritance models an "is-a" relationship by having a subclass extend a base class, inheriting its members automatically. Composition models a "has-a" relationship by building an object out of smaller, independent parts, and delegating to them instead of inheriting their behavior. Both are ways to reuse and organize behavior across types — the decision between them is one of the most consequential design choices in an object-oriented codebase, because inheritance chosen too eagerly is far harder to unwind later than composition is.

Why This Matters

Inheritance looks convenient at first: a FlyingEnemy : Enemy gets everything Enemy has for free. But a single inheritance chain forces every subclass to fit into one rigid hierarchy — the moment a design needs an enemy that both flies and swims, or a FlyingEnemy that later needs to stop flying temporarily, a strict class hierarchy has no clean way to express that without duplicating code or forcing awkward, unrelated base classes. Composition avoids this by attaching a FlightBehavior and a SwimBehavior to any object that needs them, independently of what else that object is — mixing and changing capabilities without redesigning a class hierarchy.

Mental Model

Inheritance is a family tree; composition is a toolbox. A family tree fixes your relationship to your ancestors permanently — you cannot reassign your parent, and every trait you inherit comes bundled together, whether you want all of it or not. A toolbox, by contrast, lets you pick up exactly the tools a given job needs, put one down and pick up a different one later, and combine tools freely regardless of which "family" they came from. Composition-based design (famously embodied by Unity's own Component system) treats behavior like tools in a box, attached to an object as needed, rather than baked permanently into a rigid lineage.

Core Explanation

  • Inheritance is "is-a," fixed at compile time, single-chain in C#/C++/most OOP languages. A FlyingEnemy is an Enemy — this relationship cannot change at runtime, and a class can typically extend only one base class, forcing every capability into one linear hierarchy.
  • Composition is "has-a," assembled from independent parts, often changeable at runtime. A GameObject has a HealthComponent, has a MovementComponent — these parts don't need to know about each other, can be added or removed independently, and don't require a shared ancestor.
  • The classic failure mode of deep inheritance is the "diamond" or rigid-hierarchy problem. Needing a type that combines two unrelated capabilities (flying + swimming) forces either duplicated code across two separate subclasses, or an awkward common ancestor that doesn't actually make sense for every type that needs it.
  • Composition trades some convenience for flexibility. Composed objects need explicit delegation (healthComponent.TakeDamage(...) rather than inherited TakeDamage(...)), which is a little more code up front, in exchange for capabilities that mix and change freely.

Syntax / Theory

C# — Inheritance approach

csharp
public class Enemy : MonoBehaviour { public virtual void Attack() { } }
public class FlyingEnemy : Enemy { public void Fly() { } }
public class SwimmingEnemy : Enemy { public void Swim() { } }
// A flying-AND-swimming enemy has no clean home in this hierarchy.

C# — Composition approach (Unity's own model)

csharp
public class FlightBehavior : MonoBehaviour { public void Fly() { } }
public class SwimBehavior : MonoBehaviour { public void Swim() { } }
public class HealthBehavior : MonoBehaviour { public void TakeDamage(int amount) { } }
csharp
// A single GameObject picks up exactly the components it needs.
GameObject dragonfly = new GameObject("Dragonfly");
dragonfly.AddComponent<FlightBehavior>();
dragonfly.AddComponent<SwimBehavior>(); // Flies AND swims — no hierarchy conflict.
dragonfly.AddComponent<HealthBehavior>();

C++ (Unreal — ActorComponents)

cpp
UCLASS()
class UFlightComponent : public UActorComponent { public: void Fly(); };

UCLASS()
class UHealthComponent : public UActorComponent { public: void TakeDamage(int32 Amount); };
cpp
// AActor composes behavior from independent UActorComponents, same model as Unity.
AEnemy::AEnemy()
{
    FlightComp = CreateDefaultSubobject<UFlightComponent>(TEXT("Flight"));
    HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
}

Unreal Blueprint

Attaching Actor Components in the Blueprint editor's Components panel is composition made visual — a designer builds an actor's capabilities by adding components, the same "toolbox" model as C#'s AddComponent<T>().

Practical Example

A UI system needs draggable, resizable, and closeable windows, but not every window needs every capability.

text
public interface IDraggable  { void OnDrag(Vector2 delta); }
public interface IResizable  { void OnResize(Vector2 delta); }
public interface ICloseable  { void OnClose(); }

public class DraggableBehavior : IDraggable { public void OnDrag(Vector2 delta) { /* ... */ } }
public class ResizableBehavior : IResizable { public void OnResize(Vector2 delta) { /* ... */ } }
csharp
public class Window
{
    public IDraggable Drag;   // Optional — may be null for a fixed-position window.
    public IResizable Resize; // Optional — may be null for a fixed-size window.
}

A settings window can compose DraggableBehavior without ResizableBehavior; a resizable-but-fixed-position debug console composes the opposite — no inheritance hierarchy has to anticipate every combination in advance.

Common Mistakes

  • Reaching for inheritance as the default tool for code reuse. Inheritance is appropriate for a genuine, stable "is-a" relationship that won't need to mix-and-match with unrelated capabilities later; reusing code alone is better served by composition or a shared utility.
  • Building deep inheritance chains (four, five, or more levels) "just in case." Each additional level makes the hierarchy more rigid and harder to reason about — a change to a method three levels up can have effects that are difficult to trace down through every descendant.
  • Using composition but re-deriving an inheritance-like coupling anyway, e.g. every component reaching directly into every other component's internals — composition's benefit depends on components staying genuinely independent, not just physically separate.
  • Treating this as an all-or-nothing choice. Real codebases (including Unity and Unreal's own engine architecture) mix both: a small, stable inheritance hierarchy (MonoBehaviour → Enemy) combined with composed capabilities (attached components) is normal and often the right answer, not a compromise to be avoided.

Game Development Example

An RPG needs character capabilities (melee attack, spellcasting, stealth) to combine in ways that don't map cleanly onto a class hierarchy — a rogue might have stealth and melee, a battlemage might have melee and spellcasting.

csharp
public class MeleeAttackComponent : MonoBehaviour { public void Attack() { } }
public class SpellcastComponent : MonoBehaviour { public void CastSpell(string id) { } }
public class StealthComponent : MonoBehaviour { public void EnterStealth() { } }
csharp
// Rogue: melee + stealth. Battlemage: melee + spellcasting. Assembled independently.
GameObject rogue = Instantiate(characterPrefab);
rogue.AddComponent<MeleeAttackComponent>();
rogue.AddComponent<StealthComponent>();

Trying to model this with inheritance alone (Rogue : MeleeCharacter, Battlemage : MeleeCharacter) breaks down the moment a StealthyMage is needed — composition absorbs that combination with zero hierarchy changes.

Best Practices

  • Default to composition for capabilities that might need to combine, change at runtime, or apply to otherwise-unrelated types.
  • Reserve inheritance for a genuinely stable "is-a" relationship where every subclass really does share the base's full identity, not just some of its code.
  • Keep inheritance chains shallow — two or three levels is a reasonable ceiling before reconsidering the design.
  • When unsure, ask "will this type ever need to combine with a capability from a sibling branch of the hierarchy?" — if yes, that capability belongs in a composed component, not the class hierarchy.

Exercises

  1. Take a hypothetical Enemy → FlyingEnemy → FlyingArcherEnemy three-level inheritance chain and redesign it using composition (FlightComponent, RangedAttackComponent) attached to a single Enemy base — describe what became easier to combine.
  2. Identify one capability in a game you know well that appears on otherwise-unrelated object types (e.g. "can be set on fire" applying to both characters and destructible scenery), and design a composed component for it rather than a shared base class.
  3. You're designing a vehicle system with cars, boats, and planes, where some vehicles are amphibious. Decide how much of the design should be inheritance (a shared Vehicle base) versus composition (DriveComponent, SailComponent, FlightComponent), and justify the split.
  4. Research the "favor composition over inheritance" principle's origins in the Gang of Four's Design Patterns, and explain in your own words why they still kept inheritance as a tool rather than recommending composition exclusively.
  • Inheritance — the specific mechanism this concept weighs against composition; understanding what inheritance commits you to is necessary before understanding why composition is often preferred.
  • Interfaces — composition is frequently implemented through interfaces, letting composed parts be swapped or mocked without depending on a concrete class.
  • Polymorphism — both inheritance and composition can achieve polymorphic behavior; this concept is about which structure to build that polymorphism on top of.

Further Reading

Related Concepts