Let’s see how interfaces are instrumental in organising your codebase and using composition over inheritance!
This article is also available on Medium.
When I write C#, especially for games in Unity, I usually try things a bit all over the place as a first-go. I just want my feature to work overall, and I don’t really care about the nitty-gritty details.
However, in order to get a robust and maintainable project, it’s always a good idea to refine and organise the code. And, oftentimes, it’s very pleasant when you can completely decouple unrelated systems so that they don’t collide with each other.
So, today, I want to talk a bit about OOP design patterns and C# interfaces, and see how we can build easily-interchangeable behaviours! 🙂
Composition over inheritance
C#, like many other programming languages, allows for OOP (object-oriented programming). You have a reliable system of classes, inheritance, data encapsulation, etc. But if you’re a bit used to OOP, you might know that inheritance is not the only way, in particular because of how it tightly couples everything together. That’s why some people prefer composition over inheritance.
Basically, the idea is that instead of chaining your classes via the “parent-child” link, and propagating some fields or methods before extending the child class’s behaviour, you rather build your behaviour by assembling small bricks that each contain part of the logic.
This is typically the core principle of many game engines like Unity or Unreal Engine: when you create an object in your scene, you then strap a bunch of components on it to give it a specific material, include it in the Physics system, let it play animations and link to a rig…
Now, let’s say you want to do the same in your C# code. Of course, you don’t want to just “assume” you have components A, B and C – you want to actually insure that they are available when you work in your “builder” class. This is especially important if:
- a component has an incomplete implementation (for example it contains abstract or virtual methods and you have to re-implement it yourself because they need some class-related data)
- another depends on it (like B depends on A): even if you usually try your best to decouple the different “behaviour blocks”, sometimes you still have some dependencies; in this case, the components down the dependency chain have to know that you did include the previous ones too, otherwise they won’t work!
A naive go at it!
With this in mind, a logical thing would be to list all of these “dependencies” just like you do for inheritance, right? So you’d have the various classes for your component, and then your
Builder class like this for example:
Except that if you do that, any IDE will show you a big error and the code won’t compile:
How can you solve this issue? Thanks to interfaces! 🙂
The real solution: using interfaces
In short, C# interfaces are a way of defining the “shape” of a class or a struct: the fields it should have, the methods it should define, etc. The point is (usually) not to define any actual implementation; you mostly have declarations and no bodies in your functions. You can have some virtual methods with a body (beginning with C# 8.0)… but oftentimes interfaces are more about writing up a contract with the C# compiler: whatever property the interface declares, a class that implements it has too; whatever function the interface declares, the class has and implements; whatever event or indexers the interface has, the class has.
Interfaces are a great tool for codebase sanity and structure because they allow you to predict the behaviour and features the class(es) that implement(s) them.
Note: this predictability is also nice for IDEs and autocompletion 😉
And of course, interfaces are also the key to composition in C#: contrary to classes, interfaces can be stacked in your dependency listing – in other words, we can write this:
Still, just like a class, the interfaces can contain abstract methods that you then force the programmer to re-implement in the
Builder class, some shared behaviour with virtual methods or pre-defined fields, etc.
A small example
To wrap-up this quick peek at interfaces and composition, let’s work on a small example.
Suppose we are making a sci-fi game with three base types of spaceships: fighters, cargos or ghost-riders… Except that any ship can also be any mix of these three base types! So a fighter-cargo will be able both to defend itself and store goods, while a ghost-rider fighter will have some cloaking abilities in addition to its basic cannons.
That’s one of the big advantages of this design pattern: since the logic blocks are as self-contained as possible, we can easily swap them and update the behaviour of our “builder” just by interchanging them 🙂
Anyway – let’s start with the
IFighter interface we want our
Ship to use (note: the
I prefix is a common convention for interface names):
Then, as soon as we tell the
Ship class to implement this interface, the compiler will force us to add the
Damage field and the implementation of the
This means we need to add some code to our
Ship in order to fill the contract we made previously – and so we are now absolutely certain that the
Ship class has a valid definition for the
Damage field and the
Important note: by default, interfaces declare everything as public so the
SpecialAttack() method has to be public to be recognised as the
IFighter implementation 😉
Ship class is now a valid “fighter” ship: it implements all that’s required and it has some base behaviour that comes from the
IFighter interface (namely the
We can now do just the same for the
But of course, weapons aren’t free in terms of space! A cargo that is also a fighter will necessarily have to trade a bit of space for these cannons… so we need to prepare a function to take this into account:
When we then use this interface in our
Ship class, we see that:
- we’re not required to implement the
MetalStoragefields because they have default values (so the class is okay with just using these defaults if you don’t pass in your own)
- for the
GetWeaponsStorage()method, we have to know whether the ship is a fighter or not, which can be done via pattern matching checks
This boils down to the following C#:
Finally, for the
IGhostRider, we can use an asynchronous method for the cloaking process so that the ship cloaks and then waits for a given delay before reappearing:
And don’t forget that you can decide to override some of the default values in your implementation if you want 😉
Today, I’ve talked a bit about C# interfaces and how we can use them for behaviour composition. Of course, this article was a short overview of the tool and there is a lot more to say and see!
I hope you liked this tutorial – and feel free to tell me in the comments: do you use C# interfaces a lot? Are you an “inheritance” or a “composition” fan?