Composition vs Inheritance in Unity

Although inheritance is a powerful tool, composition is quite often the better approach. This article explores what happens if composition is used whenever possible.

Source [1]

Our hypothesis is that composition is always better than inheritance.

Advantages of composition

Flexibility

Composition allows for greater code granularity and thus reuse. Logic can be modified more easily and with fewer side-effects. A significant advantage, especially while prototyping.

Encapsulation

With inheritance, subclasses have access to all of the public and protected members of their parent class, which can lead to unintended consequences and coupling between classes. If the parent changes then the child changes too.

Testability

By composing objects of small, specialized classes with fewer interfaces unit testing (faking context and data) becomes easier.

The power of composition

Composition is the way to deal with complexity.

If you are interested, look at this nice section of Brian Beckman's video "Don't fear the Monad" (min. 16:03-17:00).

Disadvantages of composition

It's cumbersome at times

Let there be objects A, B, and C. Object A requires B and B requires C.

Each of them has to be created and passed over (injected probably). That's a lot of scaffolding work that would not be required with inheritance.

class A
{
    public B b;

    public A(B b)
    {
        this.b = b;
    }
}

class B
{
    public C c;

    public B(C c)
    {
        this.c = c;
    }
}

class C
{
    public C()
    {
        public void WhatEver()
        {
            Debug.Log("meh");
        }
    }
}

class Program
{
    void Main()
    {
        var c = new C();
        var b = new B(c);
        var a = new A(b);

        // Now access C
        a.b.c.WhatEver();
    }
}

If the logic of object C needs to be accessible from outside A then A has to expose B and B has to expose C. This leads to lengthy chains of exposed objects: a.b.c.WhatEver().

In practice, problems start if A relies on some information about B's internal state. This forces B to make its internal state publicly accessible. Now the question would be why not integrate B into A if they are so tightly coupled?

The answer, it would break composition. The more we integrate, the less universally applicable our objects will get. There even might be an object X which also depends on B's logic, what then?

The big problem here is state. If A, B and C were stateless then there would be no need to expose anything except the required API. They would only consist of pure functions.

If we require state in our game (which most games do) then we will have to compromise.

Where to stop?

If everything is trimmed down to its minimal implementation then you end up with literal one-function objects without any state. The thing is we already have those, they are called pure functions.

While in theory, this gives you the most flexibility in practice too many small objects become hard to maintain. Plugging anything into anything may be super composable, but it's also a real pain to set up and debug.

When to use inheritance

Interface Polymorphism

While polymorphism can be implemented by other means than inheritance [2] it is a handy feature for interfaces. If two interfaces are to be used in a generic collection (List<T>), then they need to share the same base.

If you have got to use inheritance then use it on interfaces rather than classes.

Unity is a component-based engine

If we look at the hierarchy this becomes very obvious. From a pragmatic standpoint resistance to an engine's core concept is futile. If you lean on inheritance too much you will eventually end up with huge components containing so much logic that you will have no choice other than to do some refactoring (break them apart).

Example Phase 1 - Starting Out

Let's consider an example with a player and three enemies. Each of these should have some health, take some damage from the player if they are nearby and they should move.

This article will only show excerpts of the code. You can inspect the whole project on GitHub.

The code uses a Player and an Enemy class as well as some interfaces for abstraction. The DamageReceiverRegistry is a static class which serves as a lookup table for the player so it can find all the enemies easily.

Composition

Enemy

Since the logic of taking and dealing damage is placed in separate components (DamageReceiver, DamageDealer) the Enemy class is rather simple. It requires the two damage components and listens for incoming changes. Whenever it receives damage it will show a log message.

The DamageDealer component is not necessary to receive damage but it is logical that in the future enemies should be able to deal damage.

[RequireComponent(typeof(DamageDealer), typeof(DamageReceiver))]
public class Enemy : MonoBehaviour
{
	public IDamageDealer DamageDealer ...
	public IDamageReceiver DamageReceiver ...

	public void Awake()
	{
		DamageReceiver.OnHealthChanged += OnDamageTaken;
	}

	public void OnDamageTaken(IDamageDealer dealer, IDamageReceiver receiver, int damage)
	{
		Debug.Log($"{receiver.gameObject.name} took {damage} from {dealer.gameObject.name} and has {receiver.Health} HP left.");
	}
}

Player

The Player can receive damage and deal damage. Therefore it also requires the two damage components.

There are two variables controlling the players damage dealing ability. In a real game these would be fetched from another component. Here they are simple fields to keep things short.

The DamageReceiver component is not necessary to deal damage but it is logical that in the future the player should be able to receive damage.

[RequireComponent(typeof(DamageDealer), typeof(DamageReceiver))]
public class Player : MonoBehaviour
{
	// In real game this would be a reference to an item system returning a damage value.
	public int DamageValue = 10;
	public float DamageEffectRange = 5f;

	public IDamageDealer DamageDealer ...
	public IDamageReceiver DamageReceiver ...
	
	public void Awake()
	{
		DamageReceiver.OnHealthChanged += OnDamageTaken;
	}

	public void OnDamageTaken(IDamageDealer dealer, IDamageReceiver receiver, int damage)
	{
		Debug.Log($"{receiver.gameObject.name} took {damage} from {dealer.gameObject.name} and has {receiver.Health} HP left.");
	}

	// The list of damage receivers the player is currently hitting.
	protected List<IDamageReceiver> _damageReceivers = new List<IDamageReceiver>();

	public void DamageAllNearby()
	{
		Debug.Log($"{gameObject.name} is dealing damage.");

		DamageReceiverRegistry.FindNearby(
			positionInWorldSpace: transform.position, 
			maxDistance: 5, 
			results: _damageReceivers, 
			includeInactive: false, 
			exclude: DamageReceiver
		);
		DamageDealer.DealDamage(_damageReceivers, DamageValue);
	}
}

Inheritance

Enemy

Since all the damage dealing and damage receiving logic is inside the Enemy it is just one single component. However it is also responsible for setup tasks (registry) and event handling. Things that are handled by the damage components themselves in the composition based Enemy class.

public class Enemy : MonoBehaviour, IDamageDealer, IDamageReceiver
{
	public int MinHealth = 0;
	public int MaxHealth = 100;
	/// ... all the code that's in DamageDealer and DamageReceiver in the Composition example.

	public void Awake()
	{
		DamageReceiverRegistry.Add(this);
	}

	public void TakeDamage(IDamageDealer dealer, int damage)
	{
		Health = Mathf.Clamp(Health - damage, MinHealth, MaxHealth);

		OnHealthChanged?.Invoke(dealer, this, damage);
		OnHealthChangedEvent?.Invoke(dealer, this, damage);

		Debug.Log($"{gameObject.name} took {damage} from {dealer.gameObject.name} and has {Health} HP left.");
	}

	public void DealDamage(IEnumerable<IDamageReceiver> receivers, int damage)
	{
		foreach (var receiver in receivers)
		{
			DealDamage(receiver, damage);
		}
	}

	public void DealDamage(IDamageReceiver receiver, int damage)
	{
		receiver.TakeDamage(this, damage);
	}
}

Player

To avoid duplicating the damage dealing code the Player has to inherit from the Enemy. This is not (yet) a problem since all the functionality inside the Enemy is something the Player needs.

public class Player : Enemy
{
	// In real game this would be a reference to an item system returning a damage value.
	// If you set this to negative it will be healing.
	public int DamageValue = 10;

	// In real game this would be a reference to an item system returning a range value.
	[Min(0f)]
	public float DamageEffectRange = 5f;

	// The list of damage receivers the player is currently hitting.
	protected List<IDamageReceiver> _damageReceivers = new List<IDamageReceiver>();

	public void DamageAllNearby()
	{
		Debug.Log($"{gameObject.name} is dealing damage.");

		DamageReceiverRegistry.FindNearby(transform.position, maxDistance: 5, results: _damageReceivers, includeInactive: false, exclude: this);
		foreach (var receiver in _damageReceivers)
		{
			receiver.TakeDamage(this, DamageValue);
		}
	}
}

Conclusion

At this stage the inheritance solution is less work to set up in the Editor (fewer components to add). The code is shorter too and fewer classes are needed.

Both solutions work fine. However, the problems start to show once we try to expand the behaviour, see phase 2.

Example Phase 2 - Expanding

In phase two we expand the functionality of the enemies and the player. We add movement (rotation and translation) to them.

Composition

Adding movement to the Enemy is an easy task with additional components. We simply add the movement component to the enemy.

MovementRotation.cs

public class MovementRotation : MonoBehaviour
{
	public float Speed = 1f;

	float _progress;
	Quaternion _startRotation;

	public void Awake()
	{
		_startRotation = transform.localRotation;
	}

	public void Update()
	{
		_progress += (Time.deltaTime * Speed) % (Mathf.PI * 2f);
		transform.localRotation = _startRotation * Quaternion.Euler(0f, Mathf.Sin(_progress) * 90f, 0f);
	}
}

Combining multiple movement types (if they are not in conflict) is easy too. we simply add another component.

Inheritance

With inheritance adding more and more movement types to the Enemy leads to an ever growing chain of child classes. One for each possible combination.

public class EnemyWithMovementSinUpDown : Enemy
public class EnemyWithMovementSinUpDownAndRotation : Enemy
// ...

Granted, no one in their right mind would do it that way but this is where pure inheritance would lead you.

Another disadvantage: We have to update all our components in the scene (Enemy is now an EnemyWithMovement). This migth not be an issue once all the Enemy types are known but during prototyping, while everything changes quickly, this is a nightmare to maintain.

If the Player would also want to move, it would have to inherit from the right Enemy type. This is a very rigid system. It can not be changed at runtime.

// This one?
public class Player : EnemyWithMovementSinUpDownAndRotation

// Or rather that one?
public class Player : EnemyWithMovementSinUpDown

// Or another one?
// ...

Conclusion

Once we need to combine more than one functionality (moving + rotating) inheritance becomes impossible to maintain. The number of needed classes grows exponentially (Enemy -> EnemyWithMovementSinUpDown -> EnemyWithMovementSinUpDownAndRotation -> ...).

It also hampers our ability to prototype quickly since we have to keep replacing our old Enemy components with new extended ones.

In total, a clear win for composition.

Asset Recommendations

Whether you are or not I hope you liked this article. Here are some assets that may be useful to you. If you get one of them then some of the money will go towards funding this project. Thank you.

Disclosure: This text may contain affiliate links, which means we may receive a commission if you click a link and purchase something that we have recommended. While clicking these links won't cost you any money, they will help fund this project! The links are created by Unity and Partnerize (Unity's affiliate partner).

Related Articles

Let the AI write the code

AIs have been all the rage for a while now. And rightfully so. Language AIs like ChatGPT are powerful tools and they are here to stay.
Read article ..

Sources

  1. Meme image based on the 1992 Disney Movie Aladdin. All rights are with Disney.
  2. Stack Overflow: Types of Polymorphism https://stackoverflow.com/questions/11732422/is-polymorphism-possible-without-inheritance#answer-11732581, which itself refers to an article named "On Understanding Types, Data Abstraction, and Polymorphism" by Luca Cardelli.