C # Composition - I'm not sure I fully understand how to implement this

Okay, that's why I recently got up to speed on classes, inheritance, interfaces, and how they all interact with each other. During this, I found a general vocal disregard for inheritance and approval of the composition in various forums / blogs / videos. Well, cool is something new to learn. Using the example on this wiki page , I started playing to try to better understand this ... my achievement so far seems to only confuse myself more.

My code, then I will 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 (don't worry about it - I learned about extension methods while working on it, so I dropped them too)

 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); } } 

Ok, so the changes that I made to the base example were to change it, always using 3 interfaces, using 2-4 based on the class. However, I immediately ran into problems with how to do this using composition.

I first tried to make different types of GameObject (GameObjectAI (), GameObjectEntity (), etc.) for each type, but it just led to code duplication and all kinds of problems related to inheritance - I don’t know if I were on the right way here, but created a bad non-compositional implementation. Although, if this is so, then I definitely do not understand something about the composition, since I could not understand how to do this without creating these problems.

Then I moved on to the current solution. Although it seems grossly inelegant. Although it generates the expected result with this, it may not be right - it does have classes like Player () or Entity () that get interfaces that they don't use that should have! = Null checks in place to stop exceptions at runtime, they can obviously refer to related classes.

So yes, there is something about composition. I feel that I don’t understand now.

Thanks!

+8
inheritance c # composition
source share
1 answer

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.

+7
source share

All Articles