Before I begin, I need to point out that this post is lengthy, and is written as a kind of "journey through examples". This is for those who want to understand the reasoning behind the eventual final implementation and perhaps learn about C# along the way. If you, like me, prefer to jump straight to some source code, you can skip to the final implementation first (use the table-of-contents on the right), and perhaps come back after. :)
I'm assuming that most of you reading this will know what "standard" inheritance means in C#. It's the classic parent -> child class-extension mechanism that looks something like this:
public abstract class MusicalInstrument {
public abstract void Play();
}
public abstract class TonalInstrument : MusicalInstrument {
public abstract void Retune(Tuning tuning);
}
public class StringInstrument : TonalInstrument {
public void Restring(StringCollection strings) { /* .. */ }
}
public class WindInstrument : TonalInstrument {
public void ChangeReed(Reed reed) { /* .. */ }
}
public class Drumkit : MusicalInstrument {
public void ChangeSkins() { /* .. */ }
}
public class Piano : StringInstrument {
}
public class Flute : WindInstrument {
}
View Code Fullscreen • "Traditional Inheritance Example"
Anyway, this kind of "classical OO" inheritance is (rightly) frowned upon these days. Heavy use of inheritance has been shunned by many in the C# world in favour of things like
composition. Accordingly, inheritance has become a "dirty word" among most OOP programmers- just saying the word alone can get you booted from design meetings. ;)
There are multiple good reasons as to why this form of inheritance-based API-design is a bad idea, but one of the reasons is that however you attempt to model your target domain, certain types just tend to cross over the 'branches' of your inheritance hierarchy. Taking our musical instrument example, at some point someone might want you to add a class that represents
some weird hybrid-instrument, or perhaps you'll find out later on that
drums require tuning too. Essentially, unless the problem domain is 100% known at design-time, classical inheritance just ends up sending you down a very restrictive and binding path.
What's worse is that in the past, people tried to solve these problems by throwing more bad inheritance at it, and multiple inheritance was no exception. "For the hybrid-instrument, we can make our type inherit from StringInstrument
and WindInstrument!" they would say. This, naturally, led to even more problems such as
The Diamond of Death and didn't really solve the underlying issue at all. In fact, it made it worse.
Ultimately, the problem was with trying to model objects in the world as a series of progressive alterations and enhancements. It's easy to think of reality as being "Instrument -> Stringed -> Piano" but in actuality
Instrument may be the only valid 'base class' of all instruments, and everything else is simply a property of tangible types. For example,
Stringed is a
property of a piano. It is also
Polyphonic, and is a
Board-Zither instrument.
What's weird though, is that instead of throwing out the broken tool (single inheritance) we kept that and threw out the one that allows us to apply multiple properties to objects. It seems that people looked at the entire mess caused by trying to force everything through inheritance and noticed that MI tended to make things even worse. But that was a result of doubling down on a bad decision in the first place: Namely, encouraging all objects to stem from a single root and fit somewhere on a big 'tree' of inheritance.
Thankfully, we didn't completely throw out the multiple-inheritance paradigm; and it came back in the form of
interfaces, which allow us to label our types as having multiple properties, leaving the actual implementation down to the type itself.
For example, a more modern/idiomatic approach to our musical instruments application might look as follows:
public abstract class MusicalInstrument {
public abstract void Play();
}
public interface ITonalInstrument {
public void Retune(Tuning tuning);
}
public interface IStringInstrument : ITonalInstrument {
public void Restring(StringCollection strings);
}
public interface IWindInstrument : ITonalInstrument {
public void ChangeReed(Reed reed);
}
public interface IPolyphonicInstrument : ITonalInstrument {
public uint MaxSimultaneousNotes { get; }
}
public class Flute : MusicalInstrument, IWindInstrument {
}
public class Artemis : MusicalInstrument, IWindInstrument, IStringInstrument {
}
public class Piano : MusicalInstrument, IStringInstrument, IPolyphonicInstrument {
}
View Code Fullscreen • "Modern Composition Example"
Notice how we're no longer bound by the strict rules of inheritance. Our instruments are simply inheriting from
MusicalInstrument (which is fair enough) and then they declare all their properties with ease. For the
Artemis (that hybrid instrument in the video I linked above) we can change the strings and the reed. For the piano we can ask what the maximum number of notes playable at once is (which would be 10, ignoring smashing the keys with your head as an optional technique) as well as change the strings should we need to.
And this kind of design works just fine for a lot of applications. But what about when we want to declare multiple properties of a type
and provide a default implementation for those properties? If you think there's no need for this ever, please consider every time you've manually implemented
INotifyPropertyChanged, or told a DI container to use the same implementation for an injected property, or had to write the same tedious and error prone code for every implementation of every class that implements
IXmlSerializable.
C# doesn't really have a formal answer to this. You can write a
DefaultXYZImplementation class and then delegate most implementations of interfaces on your types to a
private readonly DefaultXYZImplementation, but doesn't that seem a bit messy? For every type that implements the interface which has a default implementation, you have to implement the methods manually, simply passing the real implementation off to your private composition object. If you're not sure what I mean, check the second 'approach' below, it shows off this inferior style.
Alternatively, there is the option of using
Dependency Injection frameworks to provide implementations of interfaces declared on types at runtime, often (though not always) through constructor-parameter-injection. This has its own drawbacks of course; the biggest being that they usually need to use reflection or compile-time IL rewriting to achieve this. The fact that this is required should be indicative that perhaps we need more language-level support for supplying default implementations on interfaces.
Of course, when we're talking about supplying 'default' implementations for interfaces, we should still be very careful. Although we're not talking about the "classical-OOP" forms of MI anymore, we're still talking about the very strong code-coupling that comes with automatically-inherited implementations. Trying to do too much with this paradigm will eventually bring us back to the original problems- and the reason why so many people still have disdain for MI as a concept.
But I'm not talking about trying to go back to the old days. I'm talking about single, atomic, 'micro-properties': Very small building blocks of implementation that can be combined with other building blocks to
compose actual types. Ones that add absolutely nothing more than they need to, and represent a single facet of an object. This concept isn't even new to the programming world; in fact, they have a name:
Mixins. Mixins, like all programming concepts, can of course be overused and abused and used in the wrong places. But used well they can actually reduce code duplication, thereby upholding the
DRY principle. And that is why I'm an advocate for multiple inheritance (at least, in this form).
Finally, if you're still unconvinced, it might interest you to know that
Java added MI functionality via 'default implementations' on interfaces not that long ago. Admittedly, it was more as a way of hacking in their new functional APIs without losing backward-compatibility, but nonetheless, if
Java, the world's slowest moving language, thinks it's a good idea... Perhaps it is. Or not. Up to you. ;)
Unfortunately C# offers no inbuilt mechanism for mixins or any other form of MI other than pure-virtual base-class inheritance (i.e. interfaces). That means that whatever approach or methodology we come up below isn't going to ever be as neat as a language-supported one. Please try to bear that in mind as we go down the rabbit-hole. ;)
I'm going to present this section as a series of examples, each with their own advantages and disadvantages: No one approach is perfect.
Finally, throughout the rest of the post I will be using a game-engine entity system as the example material; however, you don't need to be a gamer or engine programmer to understand what's going on.
Let's build an example to elucidate the problem we're actually trying to solve. As I mentioned, I'm going to build the beginning of an entity system. If you're not familiar with such a system; it's basically a programming pattern/model that's used often in game development. In a typical entity system, an
Entity object represents a single 'thing' in the game world (such as a player, or a powerup, or a projectile, or a tank... Etc.). Each
Entity is then
composed of various properties that are relevant to its type, such as "represented visually with a 3D model" and "can be collided with in the physics system", etc.
To start, let's write out our first attempt at modelling an entity system with a couple of real-world entities (at the moment I'm ignoring thread-safety concerns; let's just keep things simple and assume a single thread).
First of all, the
Entity type, the base class of all entities:
public abstract class Entity {
}
View Code Fullscreen • "Abstract Entity Base Class"
And now let's make some component types:
public abstract class TangibleEntity : Entity {
public Vector3 Position;
public Vector3 Velocity;
}
public abstract class ModelledEntity : TangibleEntity {
public ModelReference Model;
public float Scaling;
public bool CameraCanSeeThis(Camera camera) {
return !PhysicsSystem.RayTest(camera.Position, Position).IsObscured;
}
}
public abstract class AudibleEntity : TangibleEntity {
public SoundReference Sound;
public float SoundRadius;
public bool CameraCanHearThis(Camera camera) {
return SoundRadius > Vector3.Distance(camera.Position, Position);
}
}
View Code Fullscreen • "Initial Component Types"
Alright, so we have a
ModelledEntity that represents any entity with a 3D model and an
AudibleEntity that represents any entity that makes a sound. Both have a custom method that can be used to determine whether or not the player can see or hear the given entity. If you're absolutely new to game programming don't worry too much about the example implementations I've given, they're just there for the demonstration, what's important is the object graph we're trying to model.
So, now let's write some entities:
public class ArmourPowerupEntity : ModelledEntity {
private const float DEFAULT_ARMOUR_SCALING = 1f;
public ArmourPowerupEntity(Vector3 powerupPosition)
: this(powerupPosition, DEFAULT_ARMOUR_SCALING) { }
public ArmourPowerupEntity(Vector3 powerupPosition, float customScaling) {
Model = ModelDatabase.ArmourModel;
Position = powerupPosition;
Scaling = customScaling;
}
}
public class RocketProjectileEntity : ModelledEntity {
public RocketProjectileEntity(Vector3 firingPoint, Vector3 firingVelocity, bool isBigRocket) {
Model = ModelDatabase.RocketModel;
Position = firingPoint;
Velocity = firingVelocity;
Scaling = isBigRocket ? 2f : 1f;
}
}
View Code Fullscreen • "Initial Entity Types Attempt"
Alright then! We've got two very important entities representable in our game: An armour powerup and a rocket projectile. Job done!
But now, management says that they want to be able to hear the rocket hissing as it flies past the player's ear on a near-miss. And now we're kind of stuck, because C# doesn't allow this kind of multiple inheritance (where we could write something akin to
RocketProjectileEntity : ModelledEntity, AudibleEntity).
But we do want this behaviour, really, so that we can do things like use a
RocketProjectile as a
ModelledEntity when writing code concerned with its
Scale and what it looks like, and use it as an
AudibleEntity when we care about whether or not the player can hear it. And this is just the tip of the iceberg, in reality entity systems have many intertwined component types and needs!
Of course, as I mentioned above, interfaces would allow us that strongly-typed multi-use ability. We could mark
RocketProjectile as
IModelled and
IAudible... But then we have to re-implement methods like
CameraCanHearThis()/CameraCanSeeThis() and even properties like
Scale on every type that implemented those interfaces!
So... What now? Let's investigate some options. ;)
The first idea we're going to explore here is one where we add, remove, and use components of entities at rumtime, typically with methods like "
AddComponent", "
RemoveComponent", etc.
Here's our example using this approach:
// ENTITY BASE CLASS
public class Entity {
private readonly List<Component> addedComponents = new List<Component>();
public void AddComponent<TComponent>(TComponent component) where TComponent : Component {
if (HasComponent<TComponent>()) throw new ComponentAlreadyAddedException();
addedComponents.Add(component);
}
public bool HasComponent<TComponent>() where TComponent : Component {
return addedComponents.OfType<TComponent>().Any();
}
public void RemoveComponent<TComponent>() where TComponent : Component {
if (!HasComponent<TComponent>()) throw new ComponentNotAddedException();
TComponent componentToRemove = GetComponent<TComponent>();
addedComponents.Remove(componentToRemove);
}
public TComponent GetComponent<Component>() where TComponent : Component {
if (!HasComponent<TComponent>()) throw new ComponentNotAddedException();
return addedComponents.OfType<TComponent>().Single();
}
}
// COMPONENTS
public abstract class Component { }
public abstract class TangibleComponent : Component {
public Vector3 Position;
public Vector3 Velocity;
}
public class ModelledComponent : TangibleComponent {
public ModelReference Model;
public float Scaling;
public bool CameraCanSeeThis(Camera camera) {
return !PhysicsSystem.RayTest(camera.Position, Position).IsObscured;
}
}
public class AudibleComponent : TangibleComponent {
public SoundReference Sound;
public float SoundRadius;
public bool CameraCanHearThis(Camera camera) {
return SoundRadius > Vector3.Distance(camera.Position, Position);
}
}
// ENTITIES
public class ArmourPowerupEntity : Entity {
private const float DEFAULT_ARMOUR_SCALING = 1f;
public ArmourPowerupEntity(Vector3 powerupPosition)
: this(powerupPosition, DEFAULT_ARMOUR_SCALING) { }
public ArmourPowerupEntity(Vector3 powerupPosition, float customScaling) {
ModelledComponent modelledComponent = new ModelledComponent();
modelledComponent.Model = ModelDatabase.ArmourModel;
modelledComponent.Position = powerupPosition;
modelledComponent.Scaling = customScaling;
AddComponent(modelledComponent);
}
}
public class RocketProjectileEntity : Entity {
public RocketProjectileEntity(Vector3 firingPoint, Vector3 firingVelocity, bool isBigRocket) {
ModelledComponent modelledComponent = new ModelledComponent();
AudibleComponent audibleComponent = new AudibleComponent();
modelledComponent.Model = ModelDatabase.RocketModel;
modelledComponent.Position = firingPoint;
modelledComponent.Velocity = firingVelocity;
modelledComponent.Scaling = isBigRocket ? 2f : 1f;
audibleComponent.Sound = SoundDatabase.RocketSound;
audibleComponent.Position = firingPoint;
audibleComponent.Velocity = firingVelocity;
audibleComponent.SoundRadius = 1000f;
AddComponent(modelledComponent);
AddComponent(audibleComponent);
}
}
View Code Fullscreen • "Entity Component System Implementation with Runtime Composition"
This is basically
composition over inheritance being put to very good use. If we want to determine whether a given entity can be heard from a given camera, we first use
HasComponent<AudibleComponent>() to see whether the entity makes a sound at all, then we use
GetComponent<AudibleComponent>() to retrieve the
AudibleComponent, on which we can subsequently call
CameraCanHearThis(). Sorted!
...Well, this approach is well known and certainly the most popular. In fact, the widely used C#-based
Unity game engine uses a similar idea.
However, the way I see it, this design has three flaws (and one advantage!):
Advantage: We can add and remove components dynamically, on the fly, at runtime. This can be useful occasionally, especially when you don't want to map classes to entities 1-to-1.
Disadvantage: We're pushing what could, in theory, be static/compile-time information to runtime. It's possible to make a mistake somewhere and forget to check whether an entity is "audible" before treating it as such (i.e. by forgetting to call HasComponent<AudibleComponent>() before GetComponent<AudibleComponent>()). We're using a type-safe, static language; we shouldn't have to do this. It also means that if we refactor the code to remove AudibleComponent from something later on, the code still compiles (which is bad). If we were using inheritance, and removed AudibleEntity as a child class, we'd get lovely compile-time errors showing us everywhere in the code we tried to use our entity as an audible one.
Disadvantage: Even if we never forget these checks, this approach can require us to write a lot of boiler-plate when working with components. For example, instead of just being able to write "rocket.CameraCanHearThis(camera);" we need to check for and obtain the AudibleComponent first, and then call CameraCanHearThis(camera); on that instead. If you omit the check, someone might remove the component later (either by design or as a side effect of another running section of code), and you end up with exceptions.
Disadvantage: I know I said to ignore multithreaded scenarios for now, but I just want to point out that adding, checking for, getting, and using components can not be combined as an atomic operation so easily. That's not to say it's not possible, but a multithreaded entity system built like this may well become complex (e.g. what if one thread removes a component as another is using it?).
Now, personally, I'm of the opinion that the disadvantages outweigh the advantages here. The multithreading thing isn't so bad, but the pushing of what could/should be compile-time information to runtime is what sets off my unease. Also, the fact that common properties have to be set/modified simultaneously is quite nasty, and although I can think of a couple of ways around that, of varying degrees of convolution, it starts to add even
more hackery and hidden magic.
That's not to say that this approach is useless, and it's certainly more
idiomatic than where we'll be going from here, but let's explore some more options.
This second approach is the other way to implement the
composition over inheritance paradigm. In contrast to approach number one, we're going to get back compile-time information but at the cost of developer-annoyance-level. ;) Basically, we will add interfaces to our entities and then delegate their implementations to private implementation objects. Let's jump straight to the example:
// ENTITY BASE CLASS
public class Entity {
}
// COMPONENTS (INTERFACES)
public interface ITangibleEntity {
Vector3 Position { get; set; }
Vector3 Velocity { get; set; }
}
public interface IModelledEntity : ITangibleEntity {
ModelReference Model { get; set; }
float Scaling { get; set; }
bool CameraCanSeeThis(Camera camera);
}
public interface IAudibleEntity : ITangibleEntity {
SoundReference Sound { get; set; }
float SoundRadius { get; set; }
bool CameraCanHearThis(Camera camera);
}
// COMPONENTS (DEFAULT IMPLEMENTATIONS)
public class DefaultTangibleEntityImpl : ITangibleEntity {
public Vector3 Position { get; set; }
public Vector3 Velocity { get; set; }
}
public class DefaultModelledEntityImpl : IModelledEntity {
private readonly DefaultTangibleEntityImpl tangibleImpl;
public Vector3 Position {
get { return tangibleImpl.Position; }
set { tangibleImpl.Position = value; }
}
public Vector3 Velocity {
get { return tangibleImpl.Position; }
set { tangibleImpl.Velocity = value; }
}
public DefaultModelledEntityImpl(DefaultTangibleEntityImpl tangibleImpl) {
this.tangibleImpl = tangibleImpl;
}
public ModelReference Model { get; set; }
public float Scaling { get; set; }
public bool CameraCanSeeThis(Camera camera) {
return !PhysicsSystem.RayTest(camera.Position, Position).IsObscured;
}
}
public class DefaultAudibleEntityImpl : IAudibleEntity {
private readonly DefaultTangibleEntityImpl tangibleImpl;
public Vector3 Position {
get { return tangibleImpl.Position; }
set { tangibleImpl.Position = value; }
}
public Vector3 Velocity {
get { return tangibleImpl.Position; }
set { tangibleImpl.Velocity = value; }
}
public DefaultAudibleEntityImpl(DefaultTangibleEntityImpl tangibleImpl) {
this.tangibleImpl = tangibleImpl;
}
public SoundReference Sound { get; set; }
public float SoundRadius { get; set; }
public bool CameraCanHearThis(Camera camera) {
return SoundRadius > Vector3.Distance(camera.Position, Position);
}
}
// ENTITIES
public class ArmourPowerupEntity : Entity, IModelledEntity {
private const float DEFAULT_ARMOUR_SCALING = 1f;
private readonly DefaultTangibleEntityImpl tangibleImpl;
private readonly DefaultModelledEntityImpl modelledImpl;
public ModelReference Model {
get { return modelledImpl.Model; }
set { modelledImpl.Model = value; }
}
public float Scaling {
get { return modelledImpl.Scaling; }
set { modelledImpl.Scaling = value; }
}
public Vector3 Position {
get { return tangibleImpl.Position; }
set { tangibleImpl.Position = value; }
}
public Vector3 Velocity {
get { return tangibleImpl.Velocity; }
set { tangibleImpl.Velocity = value; }
}
public ArmourPowerupEntity(Vector3 powerupPosition)
: this(powerupPosition, DEFAULT_ARMOUR_SCALING) { }
public ArmourPowerupEntity(Vector3 powerupPosition, float customScaling) {
tangibleImpl = new DefaultTangibleEntityImpl();
modelledImpl = new DefaultModelledEntityImpl(tangibleImpl);
Model = ModelDatabase.ArmourModel;
Position = powerupPosition;
Scaling = customScaling;
}
public bool CameraCanSeeThis(Camera camera) {
return modelledImpl.CameraCanSeeThis(camera);
}
}
public class RocketProjectileEntity : Entity, IModelledEntity, IAudibleEntity {
private readonly DefaultTangibleEntityImpl tangibleImpl;
private readonly DefaultModelledEntityImpl modelledImpl;
private readonly DefaultAudibleEntityImpl audibleImpl;
public ModelReference Model {
get { return modelledImpl.Model; }
set { modelledImpl.Model = value; }
}
public float Scaling {
get { return modelledImpl.Scaling; }
set { modelledImpl.Scaling = value; }
}
public SoundReference Sound {
get { return audibleImpl.Sound; }
set { audibleImpl.Sound = value; }
}
public float SoundRadius {
get { return audibleImpl.SoundRadius; }
set { audibleImpl.SoundRadius = value; }
}
public Vector3 Position {
get { return tangibleImpl.Position; }
set { tangibleImpl.Position = value; }
}
public Vector3 Velocity {
get { return tangibleImpl.Velocity; }
set { tangibleImpl.Velocity = value; }
}
public RocketProjectileEntity(Vector3 firingPoint, Vector3 firingVelocity, bool isBigRocket) {
tangibleImpl = new DefaultTangibleEntityImpl();
modelledImpl = new DefaultModelledEntityImpl(tangibleImpl);
audibleImpl = new DefaultAudibleEntityImpl(tangibleImpl);
Model = ModelDatabase.RocketModel;
Position = firingPoint;
Velocity = firingVelocity;
Scaling = isBigRocket ? 2f : 1f;
Sound = SoundDatabase.RocketSound;
SoundRadius = 1000f;
}
public bool CameraCanSeeThis(Camera camera) {
return modelledImpl.CameraCanSeeThis(camera);
}
public bool CameraCanHearThis(Camera camera) {
return audibleImpl.CameraCanHearThis(camera);
}
}
View Code Fullscreen • "Entity Component System Implementation with Default Implementation Objects"
In essence, we're now declaring the interfaces properly on the types and implementing them by deferring the
real implementation to private
composition objects. In the constructor for our entities we create the implementation objects and then use them in the methods and properties that we're required to implement as part of the interface contract. The advantages and disadvantages of this approach can be summed up as follows:
Advantage: We now see type information at compile-time. We can use a RocketProjectileEntity in a list of IAudibleEntitys and detecting whether the camera can see the entity is as simple as "rocket.CameraCanSeeThis(camera)". If later on the rockets don't make a sound any more and we remove the IAudibleEntity interface, all the places where it was used as an audible entity will become compile-time errors.
Disadvantage: We're still writing a lot of boilerplate. Every class we write that uses this paradigm will have to have its interface implementation written manually, even when that implementation is just deferring to a private readonly DefaultXYZImpl field.
So we've solved some of the more egregious problems in the first approach; but still require a lot of boilerplate. The good thing, however is that the boilerplate is now in the 'private implementation' area of the code, and no longer in the areas where the entities are actually used from the outside.
This approach is more popular in the Java and enterprise worlds (and our naming convention here is very
Java-esque). It is also a nascent form of
Dependency Injection; which is generally considered a good practice. In fact, writing code in this way can allow us to use a DI
framework to inject those dependencies for us automatically- though this doesn't really solve the problem of having to defer interface implementations to the private
Impl objects. Unfortunately, I don't see (or know of) any way to really reduce boilerplate in this approach. In fact, adding a DI framework would probably just increase the noise (as DI frameworks tend to do) at this point.
So... We're closer, but there's got to be a better way. I think this idea would quickly become tedious when writing more than a handful of entity types. And although we're not repeating the actual implementation code (good) we're repeating a lot of "setup" code (bad).
Let's try something else.
Now I expect that some readers will already have seen this approach inevitably coming somewhere down the line- it's not like I'm the first person to 'discover' that extension methods can be used to kludge in a form of multiple inheritance. They're also the first approach that starts to really look like automatic, compile-time multiple inheritance.
In essence, the idea behind this approach is is to create empty interfaces and then have extension methods declared externally that actually add the functionality and methods you need.
However, extension methods as a form of MI also have a couple of fundamental flaws that neither of our previous approaches had. To demonstrate this, let's first rewrite our ongoing example, leaving some implementation details out:
// ENTITY BASE CLASS
public class Entity {
}
// COMPONENTS (INTERFACES)
public interface ITangibleEntity { }
public interface IModelledEntity : ITangibleEntity { }
public interface IAudibleEntity : ITangibleEntity { }
// COMPONENTS (EXTENSION METHOD IMPLEMENTATIONS)
public static class ComponentExtensions {
// ITangibleEntity
public static Vector3 GetPosition(this ITangibleEntity @this) {
// TODO implement this
}
public static void SetPosition(this ITangibleEntity @this, Vector3 newPosition) {
// TODO implement this
}
public static Vector3 GetVelocity(this ITangibleEntity @this) {
// TODO implement this
}
public static void SetVelocity(this ITangibleEntity @this, Vector3 newVelocity) {
// TODO implement this
}
// IModelledEntity
public static ModelReference GetModel(this IModelledEntity @this) {
// TODO implement this
}
public static void SetModel(this IModelledEntity @this, ModelReference newModel) {
// TODO implement this
}
public static float GetScaling(this IModelledEntity @this) {
// TODO implement this
}
public static void SetScaling(this IModelledEntity @this, float newScaling) {
// TODO implement this
}
public static bool CameraCanSeeThis(this IModelledEntity @this, Camera camera) {
return !PhysicsSystem.RayTest(camera.Position, GetPosition(@this)).IsObscured;
}
// IAudibleEntity
public static SoundReference GetSound(this IModelledEntity @this) {
// TODO implement this
}
public static void SetSound(this IModelledEntity @this, SoundReference newSound) {
// TODO implement this
}
public static float GetSoundRadius(this IModelledEntity @this) {
// TODO implement this
}
public static void SetSoundRadius(this IModelledEntity @this, float newSoundRadius) {
// TODO implement this
}
public static bool CameraCanHearThis(this IModelledEntity @this, Camera camera) {
return GetSoundRadius(@this) > Vector3.Distance(camera.Position, GetPosition(@this));
}
}
View Code Fullscreen • "Entity Component System Implementation with Extension Methods"
So, first of all, in case it's not obvious, this set up now theoretically allows us to use our entities like so:
// ENTITIES
public class ArmourPowerupEntity : Entity, IModelledEntity {
private const float DEFAULT_ARMOUR_SCALING = 1f;
public ArmourPowerupEntity(Vector3 powerupPosition)
: this(powerupPosition, DEFAULT_ARMOUR_SCALING) { }
public ArmourPowerupEntity(Vector3 powerupPosition, float customScaling) {
this.SetModel(ModelDatabase.ArmourModel);
this.SetPosition(powerupPosition);
this.SetScaling(customScaling);
}
}
public class RocketProjectileEntity : Entity, IModelledEntity, IAudibleEntity {
public RocketProjectileEntity(Vector3 firingPoint, Vector3 firingVelocity, bool isBigRocket) {
this.SetModel(ModelDatabase.RocketModel);
this.SetPosition(firingPoint);
this.SetVelocity(firingVelocity);
this.SetScaling(isBigRocket ? 2f : 1f);
this.SetSound(SoundDatabase.RocketSound);
this.SetSoundRadius(1000f);
}
}
// ELSEWHERE ...
public void FireRocket(Vector3 playerPosition, Vector3 playerAimDirection, bool isHyperspeedRocket) {
float rocketSpeed = isHyperspeedRocket ? 10f : 100f;
RocketProjectileEntity rocketProjectile = new RocketProjectileEntity(playerPosition, playerAimDirection.WithLength(rocketSpeed), false);
if (isHyperspeedRocket) {
rocketProjectile.SetSound(SoundDatabase.HyperspeedRocketSound);
rocketProjectile.SetSoundRadius(2000f);
}
}
View Code Fullscreen • "Extension-Method MI Usage Example"
Already we're seeing some big payoffs from this approach. Ignore for a moment the fact that we haven't yet written an implementation for the
Get()/
Set() methods (we'll get to that), here's some of the advantages of using extension methods like this:
Advantage: We still see type information at compile-time. Just like with approach #2, we can use a RocketProjectileEntity in a list of IAudibleEntitys and detecting whether the camera can see the entity is as simple as "rocket.CameraCanSeeThis(camera)".
Advantage: Not only is the information there at compile/code-writing-time; it's automatically all added for us, including the implementation! This is the true power of MI; and you can see the benefits immediately in the sheer reduction of boilerplate in our entity implementations. In fact... There's no boilerplate or implementation writing necessary at all! For example, we simply declared our rocket as IAudibleEntity and now all rockets have the state and method associated with audible entities, for free, with a sensible default implementation.
But what about the disadvantages? I mentioned above that this paradigm has some serious flaws:
Disadvantage: Because C# doesn't support extension properties (yet) we have to add fields in components in a kind of outmoded "Java" style using Get()/Set() methods. This is undoubtedly reason enough to discard this approach for some of you, but I promise that once you get over the initial shock it's not that bad.
Disadvantage: Polymorphism has gone out the window. We can no longer override the default implementations in our entities in any way (e.g. RocketProjectileEntity can not override CameraCanSeeThis() and ArmourPowerupEntity can not add validation logic to SetScale() etc.).
Disadvantage: As pointed to by the fact that I've had to leave some parts above unimplemented (with just TODO comments), there's no way to add fields that make up the adjunct, inherited state to entities using this approach.
Now, the first disadvantage is one I can think of no workaround for at the time- we'll just have to hope that Microsoft sees fit to include extension properties in an update to the language sometime in the near future. If using getters and setters in lieu of real properties irks you too much, I should warn you that I'm not about to pull something out of the bag: We're stuck here.
If you're willing to overlook that, however, let's move on to the latter two "disadvantages" ("disadvantage" is really an understatement here, let's call them what they are: Fundamental flaws). In my opinion, as long as either of these flaws exists this approach is untenable. So... Let's see if we can fix them, starting with the last one (no way to inherit fields).
So we need a way to make it so that when an entity declares itself as implementing, say,
ITangibleEntity, that the
Position and
Velocity properties are added automatically (albeit through the facade of getter and setter methods).
The first idea one might have is to use our common base class,
Entity, as somewhere to 'put' all the inherited state information. I'm going to call this state "adjunct state" as it's added to the entities by the components. Here's a simple implementation using a hash map:
// ENTITY BASE CLASS
public class Entity {
private readonly Dictionary<string, object> adjunctDataSpace = new Dictionary<string, object>();
internal T GetAdjunctField<T>(string name) {
return (T) adjunctDataSpace[name];
}
internal void SetAdjunctField<T>(string name, T value) {
adjunctDataSpace[name] = value;
}
internal T GetAdjunctField<T>(string name, Func<T> defaultValueGenerator) {
if (!adjunctDataSpace.ContainsKey(name)) adjunctDataSpace[name] = defaultValueGenerator();
return GetAdjunctField<T>(name);
}
}
// COMPONENTS (INTERFACES)
public interface ITangibleEntity { }
// COMPONENTS (EXTENSION METHOD IMPLEMENTATIONS)
public static class ComponentExtensions {
// ITangibleEntity
public static Vector3 GetPosition<T>(this T @this) where T : Entity, ITangibleEntity {
return @this.GetAdjunctField("ITangibleEntity.Position", () => Vector3.Zero);
}
public static void SetPosition<T>(this T @this, Vector3 newPosition) where T : Entity {
@this.SetAdjunctField("ITangibleEntity.Position", newPosition);
}
public static Vector3 GetVelocity<T>(this T @this) where T : Entity {
return @this.GetAdjunctField("ITangibleEntity.Velocity", () => Vector3.Zero);
}
public static void SetVelocity<T>(this T @this, Vector3 newVelocity) where T : Entity {
@this.SetAdjunctField("ITangibleEntity.Velocity", newVelocity);
}
}
View Code Fullscreen • "Extension Method MI Field Implementation One"
So this works fairly well; by using generic constraints on the extension methods we've essentially allowed ourselves to use some new methods on
Entity. Those methods store and retrieve adjunct data by way of a lookup/dictionary. Data is "pseudo-initialized" by way of the getter methods creating the value when it's first accessed.
But, this approach
still has a problem. Imagine we have a
List<ITangibleEntity> and we want to do something with the
Position value on each of them. It's as simple as
foreach (var tangible in list) DoSomething(tangible.GetPosition()); right? Wrong... Because we just have a list of tangible
somethings that don't necessarily inherit from
Entity, we can't use the extension methods anymore. So either we have to start leaking generic constraints everywhere... Or we need to find a better way.
In fact, we could do something like add the adjunct field stuff to a 'root mixin interface' that all our components inherit from... But frankly all of this is a bit of an exercise in futility. This approach could work for our specific example, but the whole
point of mixins is that they can be, well,
mixed in. We shouldn't have to require a root base class or for all types that want to use our mixins to have to implement some weird hash map stuff.
So, perhaps we need to store the adjunct data elsewhere... Let's try something else:
// ENTITY BASE CLASS
public class Entity {
}
// COMPONENTS (INTERFACES)
public interface ITangibleEntity { }
// COMPONENTS (EXTENSION METHOD IMPLEMENTATIONS)
public static class ComponentExtensions {
private static readonly Dictionary<object, Dictionary<string, object>> adjunctData = new Dictionary<object, Dictionary<string, object>>();
private static T GetAdjunctField<T>(object owningObject, string name) {
return (T) GetDataForObject(owningObject)[name];
}
private static void SetAdjunctField<T>(object owningObject, string name, T value) {
GetDataForObject(owningObject)[name] = value;
}
private static T GetAdjunctField<T>(object owningObject, string name, Func<T> defaultValueGenerator) {
var objectData = GetDataForObject(owningObject);
if (!objectData.ContainsKey(name)) objectData[name] = defaultValueGenerator();
return (T) objectData[name];
}
private static Dictionary<string, object> GetDataForObject(object obj) {
if (!adjunctData.ContainsKey(obj)) adjunctData[obj] = new Dictionary<string, object>();
return adjunctData[obj];
}
// ITangibleEntity
public static Vector3 GetPosition(this ITangibleEntity @this) {
return GetAdjunctField(@this, "ITangibleEntity.Position", () => Vector3.Zero);
}
public static void SetPosition(this ITangibleEntity @this, Vector3 newPosition) {
SetAdjunctField(@this, "ITangibleEntity.Position", newPosition);
}
public static Vector3 GetVelocity(this ITangibleEntity @this) {
return GetAdjunctField(@this, "ITangibleEntity.Velocity", () => Vector3.Zero);
}
public static void SetVelocity(this ITangibleEntity @this, Vector3 newVelocity) {
SetAdjunctField(@this, "ITangibleEntity.Velocity", newVelocity);
}
}
View Code Fullscreen • "Extension Method MI Field Implementation Two"
So now we've moved the data somewhere off on its own. This works better, but the eagle eyed of you will have spotted that we just also created the mother of all memory leaks. When before an entity object was no longer accessible from any object root (i.e. it became garbage) the adjunct data stored in the dictionary would have been reclaimed along with it. Now however, every time an object is saved to the global dictionary, the key becomes a permanent reference in our global dictionary of objects.
Damn, so close! But don't worry, there is an answer, and an easy one at that. We can replace the top-level dictionary with a
ConditionalWeakTable. This special compiler-supported type only keeps a key as long as the key is being strongly referenced elsewhere in the application. I'm not going to write another example to demonstrate this right here, just replace the first
Dictionary with a
ConditionalWeakTable. However, I
will use the
ConditionalWeakTable in the final implementation example at the bottom.
So now that we've solved that issue, let's look at how we can get back our polymorphism.
So, in case you're not sure even what the problem is here, take a look at the following example. Here the
ArmourPowerupEntity needs to implement a custom
CameraCanSeeThis() override that returns false when the armour has already been picked up:
// ENTITIES
public class ArmourPowerupEntity : Entity, IModelledEntity {
private const float DEFAULT_ARMOUR_SCALING = 1f;
private bool pickedUp = false;
public ArmourPowerupEntity(Vector3 powerupPosition)
: this(powerupPosition, DEFAULT_ARMOUR_SCALING) { }
public ArmourPowerupEntity(Vector3 powerupPosition, float customScaling) {
this.SetModel(ModelDatabase.ArmourModel);
this.SetPosition(powerupPosition);
this.SetScaling(customScaling);
}
public void PickUpArmour() {
pickedUp = true;
}
public bool CameraCanSeeThis(Camera camera) {
return !pickedUp && ComponentExtensions.CameraCanSeeThis(this, camera); // Powerup only visible if not already taken
}
}
View Code Fullscreen • "Extension-Method MI Polymorphism Problem Example"
So, what's the problem? Well, let's see what happens when we try to call our override. Assume for now that the default implementation of
CameraCanSeeThis for our powerup instance returns
true, i.e. the powerup's in view of the camera:
public void Test() {
ArmourPowerupEntity powerup = GetArmourPowerup();
Camera camera = GetCamera();
powerup.PickUpArmour();
Console.WriteLine(powerup.CameraCanSeeThis(camera)); // prints 'false'
Console.WriteLine((powerup as IModelledEntity).CameraCanSeeThis(camera)); // prints 'true'!
}
View Code Fullscreen • "Demonstration of Issue with Naive Polymorphism Implementation"
Why is this happening? Basically, it's because our 'override' didn't really
override anything, it just
hid the extension method in certain use cases. So, what now?
Well, here's where I think I'm gonna earn the most contempt; but I'm gonna go ahead and advocate a reflection-based solution. Firstly, let's look at modifying our
ComponentExtensions class. I'm going to only show what we're adding below, to keep things brief:
public static class ComponentExtensions {
private static readonly Dictionary<string, Dictionary<Type, MethodInfo>> overrides = new Dictionary<string, Dictionary<Type, MethodInfo>>();
private static bool HasOverride(object targetObj, [CallerMemberName] string callerName = null) {
if (!vtable.ContainsKey(callerName)) return false;
var overridesForMember = overrides[callerName];
var targetType = targetObj.GetType();
return overridesForMember.ContainsKey(targetType);
}
private static T InvokeOverride<T>(object targetObj, object[] parameters, [CallerMemberName] string callerName = null) {
var overridesForMember = overrides[callerName];
var targetType = targetObj.GetType();
return (T) overridesForMember[targetType].Invoke(targetObj, parameters);
}
private static void InvokeOverride(object targetObj, object[] parameters, [CallerMemberName] string callerName = null) {
var overridesForMember = overrides[callerName];
var targetType = targetObj.GetType();
overridesForMember[targetType].Invoke(targetObj, parameters);
}
// ITangibleEntity
public static Vector3 GetPosition(this ITangibleEntity @this) {
if (HasOverride(@this)) {
return InvokeOverride<Vector3>(@this, new object[] { });
}
return GetAdjunctField(@this, "ITangibleEntity.Position", () => Vector3.Zero);
}
public static void SetPosition(this ITangibleEntity @this, Vector3 newPosition) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newPosition });
return;
}
SetAdjunctField(@this, "ITangibleEntity.Position", newPosition);
}
public static Vector3 GetVelocity(this ITangibleEntity @this) {
if (HasOverride(@this)) {
return InvokeOverride<Vector3>(@this, new object[] { });
}
return GetAdjunctField(@this, "ITangibleEntity.Velocity", () => Vector3.Zero);
}
public static void SetVelocity(this ITangibleEntity @this, Vector3 newVelocity) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newVelocity });
return;
}
SetAdjunctField(@this, "ITangibleEntity.Velocity", newVelocity);
}
}
View Code Fullscreen • "Adding Reflection-Based Polymorphism, First Part"
Hopefully you're beginning to see what we're aiming for here. The default implementations will check to see if there's a more specific implementation stored in the
overrides map, and if there is, the override is executed and the default implementation returns. So the only that question remains now, is how do we populate
overrides?
Side-note: If you're concerned about creating too much garbage with respect to the
parameters paramter, simply follow
String.Format's lead and provide a few overrides with a specific number of parameters; using an object-array pool to construct the calls to
MethodInfo.Invoke(). You can also then use generics to avoid boxing (see
here for more info if you're unaware of this technique).
Also, there are some neater ways using generics and other magic to condense the boilerplate a little, but I wanted to keep things simple for now.
Anyway, here's how to get the global list of overrides at application start up. Firstly, you need to make sure that all relevant types are loaded already when this code executes. That will be dependent on your application, but it usually boils down to ensuring that the runtime has had to load every immediately dependent assembly by the point this logic runs. Your scenario may be more complex, however: If it's actually not possible to ensure this in your case, you can still implement this approach but you'll need to run the 'initialization' logic as-and-when for each new assembly that gets loaded.
For now, though, this is the code that will work for the majority of cases:
public static class ComponentExtensions {
private static readonly Dictionary<string, Dictionary<Type, MethodInfo>> overrides = new Dictionary<string, Dictionary<Type, MethodInfo>>();
internal static void PopulateOverrideTable() {
var allLoadedTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(ass => ass.GetReferencedAssemblies().Select(Assembly.Load).Concat(new[] { ass }))
.Distinct()
.SelectMany(ass => ass.GetTypes());
var allComponentExtensions = typeof(ComponentExtensions).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
.Where(mi => mi.GetCustomAttributes(typeof(ExtensionAttribute), false).Length > 0);
foreach (Type t in allLoadedTypes) {
foreach (MethodInfo extMethod in allComponentExtensions) {
string callerMemberName = extMethod.Name;
var matchingOverride = t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(mi => mi.Name == callerMemberName);
if (matchingOverride != null) {
if (!overrides.ContainsKey(callerMemberName)) overrides[callerMemberName] = new Dictionary<Type, MethodInfo>();
overrides[callerMemberName][t] = matchingOverride;
}
}
}
}
}
View Code Fullscreen • "Adding Reflection-Based Polymorphism, Second Part"
The code above is basically searching all loaded types for methods which match the name of our extension methods, and adding them to the
overrides dictionary.
The code as it is will take a long time to finish (which is why it's best done at init-time). However, if you use some way to mark your "mixin" interfaces (e.g. with a "[Mixin]" attribute) or similar, you could easily filter out all types that don't implement any of those interfaces. There are other such optimisations available, but all of these will depend on your exact application, so I'll leave that up to you. You could also compile the method info invocations to expressions for further speed, if this is a requirement.
Also if you're worried about 'false positives' (e.g. types being added to the overrides database who just happen to have a method that shares the same name), the [Mixin] attribute will stop this. It's not necessary though, as false positives will just sit in the overrides dictionary and never be called.
Additionally, the code is in a basic form right now. You might want to add more stringent criteria, such as checking that the parameters of the matching override and the extension method are the same.
And lastly, if you run obfuscation on your application, this approach may very well break down. In that case, you can try using something other than the method's name to identify it as an override, such as another custom attribute.
So, here's the final implementation; my preferred kludge to get multiple inheritance in C# (now doing away with the 'game engine' theme in lieu of a more general example):
public interface IMixinAlpha { }
public interface IMixinBeta { }
public static class ComponentExtensions {
#region MI Impl
private static readonly ConditionalWeakTable<IMixinAlpha, MixinAlphaProperties> alphaEphemerons = new ConditionalWeakTable<IMixinAlpha, MixinAlphaProperties>();
private static readonly ConditionalWeakTable<IMixinBeta, MixinBetaProperties> betaEphemerons = new ConditionalWeakTable<IMixinBeta, MixinBetaProperties>();
private static readonly Dictionary<string, Dictionary<Type, MethodInfo>> vtable = new Dictionary<string, Dictionary<Type, MethodInfo>>();
internal static void PopulateVirtualTable() {
var allLoadedTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(ass => ass.GetReferencedAssemblies().Select(Assembly.Load).Concat(new[] { ass }))
.Distinct()
.SelectMany(ass => ass.GetTypes());
var allComponentExtensions = typeof(ComponentExtensions).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
.Where(mi => mi.GetCustomAttributes(typeof(ExtensionAttribute), false).Length > 0);
foreach (Type t in allLoadedTypes) {
foreach (MethodInfo extMethod in allComponentExtensions) {
string callerMemberName = extMethod.Name;
var matchingOverride = t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(mi => mi.Name == callerMemberName);
if (matchingOverride != null) {
if (!vtable.ContainsKey(callerMemberName)) vtable[callerMemberName] = new Dictionary<Type, MethodInfo>();
vtable[callerMemberName][t] = matchingOverride;
}
}
}
}
private static bool HasOverride(object targetObj, [CallerMemberName] string callerName = null) {
if (!vtable.ContainsKey(callerName)) return false;
var overridesForMember = vtable[callerName];
var targetType = targetObj.GetType();
return overridesForMember.ContainsKey(targetType);
}
private static T InvokeOverride<T>(object targetObj, object[] parameters, [CallerMemberName] string callerName = null) {
var overridesForMember = vtable[callerName];
var targetType = targetObj.GetType();
return (T) overridesForMember[targetType].Invoke(targetObj, parameters);
}
private static void InvokeOverride(object targetObj, object[] parameters, [CallerMemberName] string callerName = null) {
var overridesForMember = vtable[callerName];
var targetType = targetObj.GetType();
overridesForMember[targetType].Invoke(targetObj, parameters);
}
private static MixinAlphaProperties GetAdjunctProperties(IMixinAlpha target) {
MixinAlphaProperties result;
if (alphaEphemerons.TryGetValue(target, out result)) return result;
MixinAlphaProperties defaultProps = new MixinAlphaProperties();
alphaEphemerons.Add(target, defaultProps);
return defaultProps;
}
private static MixinBetaProperties GetAdjunctProperties(IMixinBeta target) {
MixinBetaProperties result;
if (betaEphemerons.TryGetValue(target, out result)) return result;
MixinBetaProperties defaultProps = new MixinBetaProperties();
betaEphemerons.Add(target, defaultProps);
return defaultProps;
}
#endregion
#region Alpha Impl
private class MixinAlphaProperties {
public int AlphaInt = 10;
public string AlphaString = "Hello!";
}
public static int GetAlphaInt(this IMixinAlpha @this) {
if (HasOverride(@this)) {
return InvokeOverride<int>(@this, new object[] { });
}
return GetAdjunctProperties(@this).AlphaInt;
}
public static void SetAlphaInt(this IMixinAlpha @this, int newValue) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newValue });
return;
}
GetAdjunctProperties(@this).AlphaInt = newValue;
}
public static string GetAlphaString(this IMixinAlpha @this) {
if (HasOverride(@this)) {
return InvokeOverride<string>(@this, new object[] { });
}
return GetAdjunctProperties(@this).AlphaString;
}
public static void SetAlphaString(this IMixinAlpha @this, string newValue) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newValue });
return;
}
GetAdjunctProperties(@this).AlphaString = newValue;
}
#endregion
#region Beta Impl
private class MixinBetaProperties {
public int BetaInt = 0;
public float BetaFloat = 0f;
}
public static int GetBetaInt(this IMixinBeta @this) {
if (HasOverride(@this)) {
return InvokeOverride<int>(@this, new object[] { });
}
return GetAdjunctProperties(@this).BetaInt;
}
public static void SetBetaInt(this IMixinBeta @this, int newValue) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newValue });
return;
}
GetAdjunctProperties(@this).BetaInt = newValue;
}
public static float GetBetaFloat(this IMixinBeta @this) {
if (HasOverride(@this)) {
return InvokeOverride<float>(@this, new object[] { });
}
return GetAdjunctProperties(@this).BetaFloat;
}
public static void SetBetaFloat(this IMixinBeta @this, float newValue) {
if (HasOverride(@this)) {
InvokeOverride(@this, new object[] { newValue });
return;
}
GetAdjunctProperties(@this).BetaFloat = newValue;
}
public static bool BetaFoobar(this IMixinBeta @this) {
if (HasOverride(@this)) {
return InvokeOverride<bool>(@this, new object[] { });
}
var adjunctData = GetAdjunctProperties(@this);
return adjunctData.BetaFloat >= adjunctData.BetaInt * 2;
}
#endregion
}
View Code Fullscreen • "Final Implementation"
And then the all-important example usage:
public class ExampleTypeOne : IMixinAlpha, IMixinBeta {
public ExampleTypeOne(int alphaInt, string alphaString) {
this.SetAlphaInt(alphaInt);
this.SetAlphaString(alphaString);
}
public virtual bool BetaFoobar() {
return this.GetBetaFloat() >= this.GetBetaInt() * 3;
}
}
public class ExampleTypeTwo : ExampleTypeOne {
public ExampleTypeTwo(int alphaInt, string alphaString) : base(alphaInt, alphaString) { }
public override bool BetaFoobar() {
return this.GetBetaFloat() >= this.GetBetaInt() * 4;
}
}
static void Main(string[] args) {
ComponentExtensions.PopulateVirtualTable();
ExampleTypeOne one = new ExampleTypeOne(2, "Cool");
one.SetBetaFloat(30f);
one.SetBetaInt(10);
Console.WriteLine(one.GetAlphaInt() + one.GetAlphaString()); // "2Cool"
Console.WriteLine(one.BetaFoobar()); // "True"
ExampleTypeTwo two = new ExampleTypeTwo(3, "Spooky");
two.SetBetaFloat(30f);
two.SetBetaInt(10);
Console.WriteLine(two.GetAlphaInt() + two.GetAlphaString()); // "3Spooky"
Console.WriteLine(two.BetaFoobar()); // "False"
Console.WriteLine((one as IMixinBeta).BetaFoobar()); // "True"
Console.WriteLine((two as IMixinBeta).BetaFoobar()); // "False"
Console.ReadKey();
}
View Code Fullscreen • "Final Implementation Usage"
Some final notes:
Firstly, I decided to be a bit more specific with the way the adjunct data is kept track of than in the game engine examples. I think this way, if you can do it (i.e. you have a known, limited set of mixin types), it's much nicer, as it eliminates the amount of boxing and string keying going on.
Secondly, the whole "using dictionaries" approach can (and should) be replaced with something a bit more substantial; a double-nested dictionary is usually a sign that you need to create a new type or similar somewhere, and this is no exception- but I'll leave that as an improvement to be completed at will.
Thirdly, although my preference is to use the third approach (i.e. with the extension methods), that's not to say the first two approaches above aren't also perfectly viable. The ugly reality is that until C# supports some form of mixin/MI natively, everything we do here is going to be a bit of a hack. The reason I prefer the third approach is that it allows the
consumer of your API to continue using your types with no knowledge of the underlying mechanism, as well as making the job of the
maintainer of the API the easiest. The tradeoff, of course, is the nasty innards (with reflection) of the implementation; but apart from the unfortunate transition from properties to getters and setters, the code looks identical to the manually implemented alternative from the outside.
And finally, there are undoubtedly more implementation approaches than what I've shown here, so if you have any corrections, suggestions, criticisms, or additions, please do leave a comment! By the way, I do realise that this has been a very long post; so I should say that I expect my next one to be a lot shorter, for my own sake. ;) Peace!