This post will demonstrate the difference between using inheritance and using composition.
NOTE: The implementation of composition in this example is extremely simple. In a real project, you might want to use the Strategy Pattern, the Command pattern, or another technique, to implement the different behavior.
What is “prefer composition over inheritance”?
Sometimes, a project needs several classes that appear to be similar, but have different behavior.
In object-oriented programming, we will often handle this with inheritance. We create a base class. Then, we create sub-classes that inherit from the base class, and have the properties and functions that are unique to the sub-class.
But, that can sometimes lead to messy code.
An alternative is to use “composition”, to have a single class that can be “composed” to handle the different behaviors.
Why you would want to use it
If you were writing a game, you might have different types of monsters. The different types of monsters could be able to do different attacks – biting, kicking, or punching.
One way to build this would be to create a base Monster class, and create sub-classes for BitingMonster, KickingMonster, and PunchingMonster – with each sub-class handling the details for the different way of fighting.
But, what do you do if you have a new monster that needs to bite and kick?
Using inheritance, you might create a BitingKickingMonster. That could inherit from Monster, BitingMonster, or KickingMonster.
Inheritance versions
Let’s pretend you work at a game programming company.
You’re building a new game, and you create a Monster class, with two properties – HitPoints and AttackDamage. The code might look like this:
namespace Engine.CompositionOverInheritance.Inheritance_BEFORE { public class Monster { public int HitPoints { get; set; } public int AttackDamage { get; set; } public Monster(int hitPoints, int attackDamage) { HitPoints = hitPoints; AttackDamage = attackDamage; } } }
Then, your boss tells you the game needs to have different types of monsters. One type will attack by biting, the second by kicking, and the third by punching. So, you make the Monster class into a base class, and create three new sub-classes from it: BitingMonster, KickingMonster, and PunchingMonster.
Now, you have these classes:
Monster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class Monster { public int HitPoints { get; set; } public Monster(int hitPoints) { HitPoints = hitPoints; } } }
BitingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class BitingMonster : Monster { public int BiteDamage { get; set; } public BitingMonster(int hitPoints, int biteDamage) : base(hitPoints) { BiteDamage = biteDamage; } } }
KickingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class KickingMonster : Monster { public int KickDamage { get; set; } public KickingMonster(int hitPoints, int kickDamage) : base(hitPoints) { KickDamage = kickDamage; } } }
PunchingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class PunchingMonster : Monster { public int PunchDamage { get; set; } public PunchingMonster(int hitPoints, int punchDamage) : base(hitPoints) { PunchDamage = punchDamage; } } }
The next day, your boss tells you they need new types of monsters in the game – ones that can do different combinations of biting, kicking, and punching. Because this is getting complex, you also build a Factory class, to create the different types of monster objects.
If you continue with making sub-classes, you could end up with this code:
Monster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class Monster { public int HitPoints { get; set; } public Monster(int hitPoints) { HitPoints = hitPoints; } } }
BitingKickingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class BitingKickingMonster : BitingMonster { public int KickDamage { get; set; } public BitingKickingMonster(int hitPoints, int biteDamage, int kickDamage) : base(hitPoints, biteDamage) { KickDamage = kickDamage; } } }
BitingKickingPunchingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class BitingKickingPunchingMonster : BitingMonster { public int KickDamage { get; set; } public int PunchDamage { get; set; } public BitingKickingPunchingMonster(int hitPoints, int biteDamage, int kickDamage, int punchDamage) : base(hitPoints, biteDamage) { KickDamage = kickDamage; PunchDamage = punchDamage; } } }
BitingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class BitingMonster : Monster { public int BiteDamage { get; set; } public BitingMonster(int hitPoints, int biteDamage) : base(hitPoints) { BiteDamage = biteDamage; } } }
BitingPunchingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class BitingPunchingMonster : BitingMonster { public int PunchDamage { get; set; } public BitingPunchingMonster(int hitPoints, int biteDamage, int punchDamage) : base(hitPoints, biteDamage) { PunchDamage = punchDamage; } } }
KickingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class KickingMonster : Monster { public int KickDamage { get; set; } public KickingMonster(int hitPoints, int kickDamage) : base(hitPoints) { KickDamage = kickDamage; } } }
KickingPunchingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class KickingPunchingMonster : KickingMonster { public int PunchDamage { get; set; } public KickingPunchingMonster(int hitPoints, int kickDamage, int punchDamage) : base(hitPoints, kickDamage) { PunchDamage = punchDamage; } } }
PunchingMonster.cs
namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class PunchingMonster : Monster { public int PunchDamage { get; set; } public PunchingMonster(int hitPoints, int punchDamage) : base(hitPoints) { PunchDamage = punchDamage; } } }
MonsterFactory.cs
using System; namespace Engine.CompositionOverInheritance.Inheritance_AFTER { public class MonsterFactory { public enum MonsterType { Horse, // BitingKickingMonster Orc, // BitingKickingPunchingMonster Crocodile, // BitingMonster MikeTyson, // BitingPunchingMonster Camel, // KickingMonster Kangaroo, // KickingPunchingMonster MantisShrimp //PunchingMonster } public static Monster CreateMonster(MonsterType monsterType) { switch(monsterType) { case MonsterType.Horse: return new BitingKickingMonster(10, 5, 5); case MonsterType.Orc: return new BitingKickingPunchingMonster(10, 5, 5, 5); case MonsterType.Crocodile: return new BitingMonster(10, 5); case MonsterType.MikeTyson: return new BitingPunchingMonster(10, 5, 5); case MonsterType.Camel: return new KickingMonster(10, 5); case MonsterType.Kangaroo: return new KickingPunchingMonster(10, 5, 5); case MonsterType.MantisShrimp: return new PunchingMonster(10, 5); default: throw new ArgumentException(); } } } }
If you use a factory class to instantiate objects, it might look like this – instantiating the objects with the required sub-class.
TestMonsterFactory.cs
using Engine.CompositionOverInheritance.Inheritance_AFTER; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TestEngine.CompositionOverInheritance.Inheritance { [TestClass] public class TestMonsterFactory { [TestMethod] public void Test_BitingMonster() { Monster crocodile = MonsterFactory.CreateMonster(MonsterFactory.MonsterType.Crocodile); Assert.IsTrue(crocodile is BitingMonster); } [TestMethod] public void Test_BitingKickingMonster() { Monster horse = MonsterFactory.CreateMonster(MonsterFactory.MonsterType.Horse); Assert.IsTrue(horse is BitingMonster); // This test will fail, because we cannot inherit from multiple base classes. Assert.IsTrue(horse is KickingMonster); } } }
Notice that we run into a problem when a new class needs to inherit from more than one base class. For example, the BitingKickingMonster inherits from BitingMonster, and duplicates the KickDamage property – because it could not also inherit from KickingMonster.
If these classes were in a real program, and not a simple demonstration, there would probably be much more duplication of properties and functions.
There is another problem. If we try to determine the “type” of an object, to determine what attacks it can perform, we can only check against the single base class – not the second class.
After you finish creating all these classes, your boss sends you an email. Now, you need to have monsters that can also attack by spitting.
Cobras will bite and spit, camels will kick and spit, etc.
Instead of creating more sub-classes, you decide to try using composition.
Composition version
The definition of “Composition” is, “combining parts or elements to form a whole”.
In this case, we will have a single Monster class, and “compose” it with the appropriate attack behavior for each type of monster.
Monster.cs
using System; using System.Collections.Generic; namespace Engine.CompositionOverInheritance.Composition_BEFORE { public class Monster { public enum AttackType { Biting, Kicking, Punching } public int HitPoints { get; set; } public Dictionary<AttackType, int> AttackTypes { get; set; } // These replace the functionality of checking an object's "type", // to see if it "is" a certain datatype (KickingMonster, BitingMonster, etc.) public bool CanBite => AttackTypes.ContainsKey(AttackType.Biting); public bool CanKick => AttackTypes.ContainsKey(AttackType.Kicking); public bool CanPunch => AttackTypes.ContainsKey(AttackType.Punching); public int BiteDamage { get { if(CanBite) { return AttackTypes[AttackType.Biting]; } throw new NotSupportedException("This monster cannot bite."); } } public int KickDamage { get { if(CanKick) { return AttackTypes[AttackType.Kicking]; } throw new NotSupportedException("This monster cannot kick."); } } public int PunchDamage { get { if(CanPunch) { return AttackTypes[AttackType.Punching]; } throw new NotSupportedException("This monster cannot punch."); } } public Monster(int hitPoints) { HitPoints = hitPoints; AttackTypes = new Dictionary<AttackType, int>(); } public void AddAttackType(AttackType attackType, int amountOfDamage) { AttackTypes[attackType] = amountOfDamage; } } }
With this Monster class, when we create a new Monster object, we “compose” its Attack options by calling the AddAttackType() function – with the AttackType, and the amount of damage the monster does with this attack.
I added a few more properties (CanBite, CanKick, and CanPunch), to make it easy to know what types of attacks a monster can perform.
This one class also has all the Damage properties in it (BiteDamage, KickDamage, and PunchDamage).
With these six properties, we can compose the Monster object to attack however we want – which we do in the MonsterFactory class below.
MonsterFactory.cs
using System; namespace Engine.CompositionOverInheritance.Composition_BEFORE { public class MonsterFactory { public enum MonsterType { Horse, // BitingKickingMonster Orc, // BitingKickingPunchingMonster Crocodile, // BitingMonster MikeTyson, // BitingPunchingMonster Camel, // KickingMonster Kangaroo, // KickingPunchingMonster MantisShrimp //PunchingMonster } public static Monster CreateMonster(MonsterType monsterType) { Monster monster; switch(monsterType) { case MonsterType.Horse: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Kicking, 5); break; case MonsterType.Orc: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Kicking, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.Crocodile: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); break; case MonsterType.MikeTyson: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.Camel: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Kicking, 5); break; case MonsterType.Kangaroo: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Kicking, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.MantisShrimp: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Punching, 5); break; default: throw new ArgumentException(); } return monster; } } }
This is a little more complex than the previous factory, because this is where we “compose” the Monster object, to act like a BitingMonster object (or, whatever attacks the monster can perform).
TestMonsterFactory.cs
using Engine.CompositionOverInheritance.Composition_BEFORE; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TestEngine.CompositionOverInheritance.Composition { [TestClass] public class TestMonsterFactory { [TestMethod] public void Test_BitingMonster() { Monster crocodile = MonsterFactory.CreateMonster(MonsterFactory.MonsterType.Crocodile); Assert.IsTrue(crocodile.CanBite); Assert.IsFalse(crocodile.CanKick); Assert.IsFalse(crocodile.CanPunch); } [TestMethod] public void Test_BitingKickingMonster() { Monster horse = MonsterFactory.CreateMonster(MonsterFactory.MonsterType.Horse); Assert.IsTrue(horse.CanBite); Assert.IsTrue(horse.CanKick); Assert.IsFalse(horse.CanPunch); } } }
In these unit tests, we can use the CanBite, CanKick, and CanPunch properties to simulated checking for the “type”, like we would in the inheritance version.
With the composition method, when our boss tells us to add a new “Spit” attack, instead of creating new classes, we would only need to:
- Add a new “Spitting” value to the AttackType enum in Monster.cs
- Add a new “CanSpit” property to Monster.cs
- Add a new “SpitDamage” property to Monster.cs
Then, MonsterFactory.cs can create monsters that can use the new “spit” attack.
The code might look like this:
Monster.cs
using System; using System.Collections.Generic; namespace Engine.CompositionOverInheritance.Composition_AFTER { public class Monster { public enum AttackType { Biting, Kicking, Punching, Spitting } public int HitPoints { get; set; } public Dictionary<AttackType, int> AttackTypes { get; set; } // These replace the functionality of checking an object's "type", // to see if it "is" a certain datatype (KickingMonster, BitingMonster, etc.) public bool CanBite => AttackTypes.ContainsKey(AttackType.Biting); public bool CanKick => AttackTypes.ContainsKey(AttackType.Kicking); public bool CanPunch => AttackTypes.ContainsKey(AttackType.Punching); public bool CanSpit => AttackTypes.ContainsKey(AttackType.Spitting); public int BiteDamage { get { if(CanBite) { return AttackTypes[AttackType.Biting]; } throw new NotSupportedException("This monster cannot bite."); } } public int KickDamage { get { if(CanKick) { return AttackTypes[AttackType.Kicking]; } throw new NotSupportedException("This monster cannot kick."); } } public int PunchDamage { get { if(CanPunch) { return AttackTypes[AttackType.Punching]; } throw new NotSupportedException("This monster cannot punch."); } } public int SpitDamage { get { if(CanSpit) { return AttackTypes[AttackType.Spitting]; } throw new NotSupportedException("This monster cannot spit."); } } public Monster(int hitPoints) { HitPoints = hitPoints; AttackTypes = new Dictionary<AttackType, int>(); } public void AddAttackType(AttackType attackType, int amountOfDamage) { AttackTypes[attackType] = amountOfDamage; } } }
MonsterFactory.cs
using System; namespace Engine.CompositionOverInheritance.Composition_AFTER { public class MonsterFactory { public enum MonsterType { Horse, // BitingKickingMonster Orc, // BitingKickingPunchingMonster Crocodile, // BitingMonster MikeTyson, // BitingPunchingMonster Cobra, // BitingSpittingMonster Camel, // KickingSpittingMonster Kangaroo, // KickingPunchingMonster MantisShrimp //PunchingMonster } public static Monster CreateMonster(MonsterType monsterType) { Monster monster; switch(monsterType) { case MonsterType.Horse: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Kicking, 5); break; case MonsterType.Orc: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Kicking, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.Crocodile: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); break; case MonsterType.MikeTyson: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.Cobra: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Biting, 5); monster.AddAttackType(Monster.AttackType.Spitting, 5); break; case MonsterType.Camel: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Kicking, 5); monster.AddAttackType(Monster.AttackType.Spitting, 5); break; case MonsterType.Kangaroo: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Kicking, 5); monster.AddAttackType(Monster.AttackType.Punching, 5); break; case MonsterType.MantisShrimp: monster = new Monster(10); monster.AddAttackType(Monster.AttackType.Punching, 5); break; default: throw new ArgumentException(); } return monster; } } }
Notice that the Camel can now kick and spit. There is also a new monster: the cobra, which can bite and spit.
To give the Camel a new attack ability, we only needed to add one line (line 53).
To add the cobra, we only needed to add it to the enum, and add a new “case” (starting at line 45) to create the cobra – by composing a monster that can bite and spit.
Where I’ve found it useful
There are two common situations when you would want to consider using composition, instead of inheritance: when you need to do multiple inheritance, and when your sub-classes start to have their own sub-classes.
These are big indicators that the composition might be a better choice.
You might also combine object composition with the Strategy Design Pattern. Instead of composing the object with property values (the attack type and damage), you could also compose it by configuring how it will perform its actions.
Source code for my design pattern lessons
28 Comments