In a previous post, we learned implementation inheritance should be avoided. But, what can we do instead?
Composition to the Rescue
We want to be able to share code while allowing us to switch out different functionalities on the fly keeping us extensible. Without inheritance, what could we do to share code? Let’s use composition!
In the above diagram, the diamond is a sign of composition. We can read this as, “A Snowball has a Renderable component.” In code, we would use a private Renderable data member inside of Snowball to say Snowball is composed of a renderable object.
We still have the same benefits of mix-ins – we can mix and match any classes together to create what we want. We also gain more extensibility than ever because we can still use interface inheritance to achieve polymorphism. Let’s explore this by seeing how we could add a circular collider for our Snowball.
Our objects code to the interface of Collidable. This usage of polymorphism and composition is called the Strategy Pattern. Our objects are agnostic to what type of Collidable they have. The encapsulation of type numerous benefits:
- We don’t need to modify the object’s code doesn’t when changing the type of collision detection we want.
- When testing Snowball, we can now mock out the type of Collidable we have. The decoupling makes the tests for Snowball very clean and proves that our code is extensible.
- If we want to add a new type of collision later, we can add it without modifying any existing code, adhering to the open-closed principle, keeping our old code safe from any new bugs.
Note: A lot of these advantages rely on us separating use from creation (don’t new the object where you use the object or else you’ll be coupled to its subtype AND its interface making it harder to take advantage of the strategy pattern). Use dependency injection to pass in the type of Collidable into the object class or instantiate the type of Collidable inside of a factory.
You might be thinking, “Don’t I have to write lines of code for each of my object classes now because I need to add the data member and function call that routes to the appropriate component class? Wouldn’t inheritance allow me to write those methods once?” This is true. Overall, however, the total lines of code in your program will be saved, and the readability and extensibility benefits far outweigh the lines for composition.
If you’re still not convinced, notice how Player, Snowball, and Wall are all pretty similar and if you could see the code, you would notice that all they do is call their component classes to do work. We could simplify this and never need to make another object class again!
Now you can create any game object by just combining different types of Movable, Renderable, and Collidable together.
We could even go further by noticing that Collidable, Movable, and Renderable are all “Components,” and a GameObject is just a list of Components that work together. This is the fundamental idea behind game engines like Unity. You just add any components you want to a game object, and you make magic happen! This system is super flexible all because of the power of composition and polymorphism!
One caveat: the more generic you make your code, the harder it is to keep it readable. Be careful. Keep in mind tips to keep your code readable like great naming and keeping it as simple as it needs to be.
Use composition instead implementation inheritance and use polymorphism to create extensible code. Composition makes your code far more extensible and readable than inheritance ever would. Be very careful when you use inheritance. It should never be the first option you think of, but there are some design patterns which use inheritance to achieve certain goals. Remember that anytime you use inheritance, you are losing extensibility which is usually a bad thing. Don’t fall into the same mistakes that I made when I was younger.
If you loved this post, you would enjoy reading most of the books I recommend. Check them out!