Composition? Let’s first see if inheritance is the right tool. These interfaces are behavior or characteristics. The easiest way to implement multiple actions is to simply inherit from these interfaces. For example (simplified code):
sealed class Enemy : IMoveable, ICollidable { public void Move() { } public void Collide() { } }
The biggest drawback of this (as it is) is that you will have some code duplication (the hypothetical classes Netrual and Friend will have to rewrite the same Enemy logic).
There are some workarounds for this; the simplest one is based on the assumption that you probably have object classes that have some characteristics. Often you do not need composition if you can build a good inheritance hierarchy. For example, the moved object also has a bounding box, then it can check for conflicts:
abstract class PhysicalObject : IMoveable, ICollidable { public virtual void Move() { } public virtual void Collide() { } } abstract class SentientEntity : PhysicalObject, IAi { public virtual void Ai() { } } sealed class Enemy : SentientEntity { } sealed class Player : SentientEntity { } sealed class Friend : SentientEntity { }
Each derived class can override the default behavior defined in the base class. I will try as much as possible (up to language restrictions) to use inheritance to describe the relationships and composition of IS-A to describe HAS-A relationships. Single inheritance will limit us or lead to code duplication. However, you can resort to our first implementation and delegate the task to a separate object, it's time to introduce the composition:
sealed class Enemy : IMoveable, ICollidable { public void Move() => _moveable.Move(); private readonly IMoveable _moveable = new Moveable(); }
In this way, code from the Moveable implementation of IMoveable can be shared and reused between different specific classes.
Sometimes this is not enough. You can combine both methods:
abstract class PhysicalObject : IMoveable, ICollidable { protected PhysicalObject() { _moveable = CreateMoveableBehavior(); _collidable = CreateCollidableBehavior(); } public void Move() => _moveable.Move(); public void Collide() => _collidable.Collide(); protected virtual IMoveable CreateMoveableBehavior() => new Moveable(); protected virtual ICollidable CreateCollidableBehavior() => new Collidable(); private readonly IMoveable _moveable; private readonly ICollidable _collidable; }
In this way, derived classes can provide their own specialized implementation of these behaviors. For example, a ghost (assuming that in our gameplay the ghost is a physical object) may encounter anything else with a 1/10 probability:
sealed class Ghost : PhysicalObject { protected override CreateCollidableBehavior() => new ColliderWithProbability(0.1); }
What if your game engine needs to handle collisions without physical contact (for example, between two charged particles)? Just write ad-hoc behavior and it can be applied anywhere (for example, to handle electromagnetism and gravity).
Now everything is becoming more interesting. You may have a complex object consisting of several parts (for example, an airplane made by its body and wings, which may have, say, other resistance to weapons). A simplified example:
abstract ComposedPhysicalObject : ICollidable { public void Collide() { Parts.ForEach(part => part.Collide()); } public List<ICollidable> Parts { get } = new List<ICollidable>() }
In this case, the list implements ICollidable , then it forces all parts to have this behavior. This may not be true:
interface IGameObject { } interface ICollidable : IGameObject { } abstract ComposedPhysicalObject : ICollidable { public void Collide() { foreach (var part in Parts.OfType<ICollidable>()) part.Collide(); } public List<IGameObject> Parts { get } = new List<IGameObject>() }
Composed objects can then even override the default behavior of their parts or add / expand it (a set often does not have the same properties of its individual elements).
We can write another 1000 examples of both more complex and simpler ones. In general, I would advise you not to make your architecture too complex if you do not need it (especially for games ... abstractions have a price in performance). You made the right choice of your architecture using a test, IMO is the most important part of TDD : if you write your tests, you see that the code is more complex than it should be, then you need to change your architecture; if you write a test first and run the architecture, it will be more domain oriented and easy to understand. It's good that the ideal case ... our choice language sometimes forces us to choose one solution instead of another (for example, in C # we do not have multiple inheritance ...)
I think (but this is only my opinion) that you should not “learn about composition” without using where it is really necessary. Composition and inheritance are just tools (like BTW) that you choose to model your domain , and you probably need to first understand when to use them in the “right” case, or you will use them in the future because of their effect instead of their meaning.
In your example ... why is it suboptimal? Since you are trying to add some behaviors (move, collide, etc.) to the base class, when they may not apply, and you do not reuse any code. You create a composition of behavior that may or may not be implemented (why do you check null before calling these methods, BTW can it be simplified to fieldName?.MethodName() ). At runtime, you decide something that is well known at compile time, and compile-time checks are (almost) always preferable.
If every object needs to implement this logic (I would prefer to avoid this) to simplify the dial-peer, you don't need all this complexity:
abstract class GameObject { public virtual void Move() { } public virtual void Collide() { } } sealed class Player : GameObject { public override void Move() => _collisionLogic.Move(); private readonly CollisionLogic _collisionLogic = new CollisionLogic(this); }
In the general composition and inheritance, they are not mutually exclusive, and they play best when used together.