Okay so I have recently been getting up to speed on Classes, Inheritance, Interfaces and how they all interact with one another. During this I discovered a general vocal disdain for inheritance and a favouring of Composition on various forums/blogs/videos. Okay, cool something new to learn. Using the example on this wiki page I set about playing around to try and better understand it... my achievement so far seems to be only confusing myself more.
My code, then I'll explain what I think is wrong.
MainClass.cs
class MainClass
{
static void Main(string[] args)
{
var player = new Player();
var enemy = new Enemy();
var entity = new Entity();
for(int i = 0; i < GameObject.GameObjects.Count; i++)
{
GameObject.GameObjects[i].Type();
GameObject.GameObjects[i].Draw();
GameObject.GameObjects[i].Move();
GameObject.GameObjects[i].Collision();
GameObject.GameObjects[i].Ai();
Console.WriteLine();
}
Console.ReadKey();
}
}
GameObject.cs
interface IVisible
{
void Draw();
}
class Visible : IVisible
{
public void Draw()
{
this.Print("I am visible!");
}
}
class Invisible : IVisible
{
public void Draw()
{
this.Print("I am invisible!");
}
}
interface IVelocity
{
void Move();
}
class Moving : IVelocity
{
public void Move()
{
this.Print("I am moving!");
}
}
class Stopped : IVelocity
{
public void Move()
{
this.Print("I am stopped!");
}
}
interface ICollidable
{
void Collide();
}
class Solid: ICollidable
{
public void Collide()
{
this.Print("I am solid!");
}
}
class NotSolid: ICollidable
{
public void Collide()
{
this.Print("I am not solid!");
}
}
interface IAi
{
void Ai();
}
class Aggressive : IAi
{
public void Ai()
{
this.Print("I am aggressive!");
}
}
class Passive : IAi
{
public void Ai()
{
this.Print("I am passive!");
}
}
class GameObject
{
private readonly IVisible _vis;
private readonly IVelocity _vel;
private readonly ICollidable _col;
private readonly IAi _ai;
public static List<GameObject> GameObjects = new List<GameObject>();
public GameObject(IVisible visible, IVelocity velocity)
{
_vis = visible;
_vel = velocity;
GameObjects.Add(this);
}
public GameObject(IVisible visible, IVelocity velocity, ICollidable collision)
{
_vis = visible;
_vel = velocity;
_col = collision;
GameObjects.Add(this);
}
public GameObject(IVisible visible, IVelocity velocity, ICollidable collision, IAi ai)
{
_vis = visible;
_vel = velocity;
_col = collision;
_ai = ai;
GameObjects.Add(this);
}
public void Draw()
{
if(_vis != null)
_vis.Draw();
}
public void Move()
{
if(_vel != null)
_vel.Move();
}
public void Collision()
{
if(_col != null)
_col.Collide();
}
internal void Ai()
{
if(_ai != null)
_ai.Ai();
}
}
class Player : GameObject
{
public Player() : base (new Visible(), new Stopped(), new Solid()) { }
}
class Enemy : GameObject
{
public Enemy() : base(new Visible(), new Stopped(), new Solid(), new Aggressive()) { }
}
class Entity : GameObject
{
public Entity() : base(new Visible(), new Stopped()) { }
}
Utilities.cs (wouldn't worry about this - I found out about extension methods whilst working on this so threw them in as well)
public static class Utilities
{
public static void Type(this object o)
{
Console.WriteLine(o);
}
public static void Print(this object o, string s)
{
Console.WriteLine(s);
}
}
Okay so the changes I made to the base example were to change it from always using 3 interfaces to it using 2-4 based upon the class. However I immediately ran into problems of how to do this via composition.
I first tried making different types of GameObject (GameObjectAI(), GameObjectEntity() etc) for each type, but this just seemed to lead to code duplication and all the sorts of wonky issues associated with inheritance - I don't know if I was on the right track here but created a bad non-compositional implementation. Though if that is the case then I am definitely not understanding something about composition as I couldn't see how to do it without creating those issues.
Then I moved to the current solution. Though this seems grossly inelegant. Whilst it produces the expected output with this, this can't be right - it has classes like Player() or Entity() getting interfaces they don't use which then have to have != null checks in place to stop runtime exceptions as they can obviously have their associated classes called.
So yeah, there is something about composition I feel like I'm not getting right now.
Thanks!
Composition? Let's first see if inheritance is the right tool. Those interfaces are behaviors or characteristics. Simplest way to implement multiple behaviors is to simply inherit from those interfaces. For example (simplified code):
sealed class Enemy : IMoveable, ICollidable {
public void Move() { }
public void Collide() { }
}
The biggest disadvantage of this (as is) is that you will have some code duplication (hypothetical Netrual
and Friend
classes will need to rewrite the same logic of Enemy
).
There are some workarounds for this, simplest one is based on the assumption that you probably have classes of objects which share some characteristics. Often you won't need composition if you can build a good inheritance hierarchy. For example a moveable object also has a bounding box then it can check for collisions:
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 might override default behavior defined in the base class. I'd try, as much as possible (up to the language limits) to use inheritance to describe IS-A relations and composition to describe HAS-A relations. Single inheritance will limit us or will cause some code duplication. You can, however, resort our first implementation and delegate the job to a separate object, it's time to introduce composition:
sealed class Enemy : IMoveable, ICollidable {
public void Move() => _moveable.Move();
private readonly IMoveable _moveable = new Moveable();
}
In this way code from Moveable
implementation of IMoveable
can be shared and reused between different concrete classes.
Sometimes that's not enough. You can combine both techniques:
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 may provide their own specialized implementation of those behaviors. For example a ghost (assuming that in our gameplay a ghost is a physical object) may collide with anything else with a 1/10 probability:
sealed class Ghost : PhysicalObject {
protected override CreateCollidableBehavior()
=> new ColliderWithProbability(0.1);
}
What if in your game engine you need to handle collisions without physical contact (for example between two charged particles)? Just write an ad-hoc behavior and it can be applied anywhere (for example to handle electromagnetism and gravity).
Now things start to be more interesting. You may have a composed object made of multiple parts (for example an airplane made by its body and wings which may have, let's say, a different resistance to weapons). 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 the parts to have this behaviors. It might 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 may then even override the default behavior of their parts or to add/extend it (the set has often not the same properties of its separate elements).
We may write 1,000 more examples both more complex or simpler. In general I'd suggest to do not make your architecture overcomplicate unless you really need it (especially for games...abstractions have a price in performance). You did it right validating you architecture with a test, IMO is the most important part of TDD: if writing your tests you see that code is more complex than it should be then you need to change your architecture; if you first write the test and architecture follows then it will be more domain oriented and easy to understand. OK, that's the ideal case...our language of choice will sometimes force us to pick one solution instead of another (for example in C# we do not have multiple inheritance...)
I think (but that's just my opinion) that you shouldn't "learn about composition" without an use-case where it's really required. Composition and inheritance are just tools (like patterns BTW) you pick to model your domain and you probably need to first understand when to use them in the "right" case or you will (ab)use them in future because of their effect instead of their meaning.
In your example...why it is suboptimal? Because you're trying to add some behaviors (move, collide, etc) to the base class when they may not apply and you're not reusing any code. You're creating a composition of behaviors which may or may not be implemented (that's why you check for null
before invoking those methods, BTW it may be simplified to fieldName?.MethodName()
). You're resolving at run-time something that is perfectly known at compile-time and compile-time checks are (almost) always preferable.
If every object has to implement that logic (I'd rather avoid that) to simplify calling point then you do not need all that 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 general composition and inheritance do not exclude each other and they play best when used together.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With