This post is part of a nine post series about the SOLID principles and code architecture. You can check all the posts here:
- Software Architecture in Game Development
- The Single Responsibility Principle
- The Open Closed Principle
- The Liskov Substitution Principle
- Conceptual Meaning Of Interfaces
- The Interface Segregation Principle
- The Dependency Inversion Principle
- SOLID: How to use it, Why and When
- SOLID For Unity Monobehaviours
Introduction
Before I continue with the SOLID principles and the Interface segregation principle, I think an explanation of the usage of Interfaces is in order. I am not talking about the technical aspect of interfaces, but an explanation of why interfaces are so useful.
Interfaces are the primary way of creating abstractions in C#, but why abstractions are necessary in the architecture of our code? Interfaces are usually described as a contract, between the class that implements them and the class that uses them. Interfaces also help with the encapsulation of our classes, help with the usage of dynamic polymorphism and make our code less rigid by providing a way to change our dependencies without affecting implementations that represent real world concepts.
But first, let’s quickly see an overview of the technical side of interfaces in C#, before trying to understand their conceptual meaning.
The Technical Explanation Of Interfaces
Inheritance is a basic concept of OOP. A class, usually called child or derived class, inherits data and behaviours of another class, usually referred as a base or parent class.
In other languages, the concept of multiple inheritance exists. A class can inherit data and behaviours from multiple parent classes. This can create the Diamond problem. Although this problem is easy to solve, with a number of solutions and in fact C# has a solution for it after allowing default interface implementations, C# only allows a class to inherit from one other class.
In a language that allows multiple inheritance, for example C++, we can have pure abstract classes as parents. A pure abstract class is a class that has only abstract methods. Abstract methods have only the signature of the method, but no implementations. The classes that inherit from that class are responsible for providing implementations for these methods.
In C# that would present a problem. By allowing inheritance from only one class, we would not be able to have these abstractions. For this reason we have the concept of interfaces.
Interfaces are the way in C# that we can inherit from multiple classes as long only one of those classes provides implementations.
In other words interfaces in C#, are pure abstract classes. Classes that all their methods are abstract. (Default interface implementations are an exception to that, but exist as a way to keep compatibility with an existing API and not as a way to have multiple inheritance in C#)
What is so important with abstractions, that C# decided to allow inheritance from multiple pure abstract classes in the form of interfaces?
Interfaces as a Contract
You may have heard or read that interfaces are a contract. A contract between the class that implements the interface and the class that uses it. Let’s try to dissect that, what exactly is a contract and who is responsible for it.
An example of a contract in the real world, is a contract between an employer and an employee. The contract describes what an employee should do. For example: I can code, cook, watch movies and write blog posts, but my employee doesn’t care about all that. He only cares that I can code. He has a company that looks for someone that can code, so he creates the requirements and describes them in a paper. Any other things I can do are not part of those requirements as my employer has no need for them.
On the other hand, my employer doesn’t care how I would code something. Obviously he cares that I do it correctly, but the way I choose to do it, is on me, as long as the final result is within the requirements described in the contract.
My employer is the class that uses the contract. I am the class that implements that contract. The contract does not describe everything that I can do, only the things that are of interest to my employer.
Although I am the one that implements the contract, the one responsible for drafting it is obviously my employer. I do not go looking for a job and say “Hi, I am here to code, cook and watch movies”. My employer says that he is looking for someone that can code and he only cares about my coding skills.
Conceptually, the contract belongs more to my employer who is responsible for drafting it than me, that I have the responsibility of implementing it.
The same is true for our code. Conceptually an interface belongs to the class or system that is using it and not to the class or classes that implement it.
Let’s see an example in code.
A C# example of a Contract
Let’s suppose that we have enemies in a game that when they attack, the player character gives a warning. We may be tempted to do something like this:
public class Enemy
{
private readonly IPlayer _player;
public Enemy(IPlayer player) => _player = player;
public void Attack()
{
// Enemy attack code
_player.EnemyAttackWarning();
}
}
public class Player : IPlayer
{
public void EnemyAttackWarning() => Console.WriteLine("An Enemy is Attacking!");
}
public interface IPlayer
{
void EnemyAttackWarning();
}
Here the Player
class implements the IPlayer
interface, the player instance is injected as an IPlayer
through the Enemy
constructor and the Enemy
class uses the IPlayer
to execute the warning.
The problem here, is that coded like this, the IPlayer
interface conceptually is closer to the Player
class than it is to the Enemy
class.
We can have the same code with just one small change. We should rename the IPlayer
interface to something more appropriate that is conceptually closer to the Enemy
and gives the reader a better understanding of its purpose.
For example we could have called it IEnemyWarnings
. The code might be the same, but there is a big difference: If ever the requirements about the enemy attack warnings change, the only thing we have to do is to have the appropriate class implement the IEnemyWarnings
interface.
For example, if we ever decided that an NPC should give the warning, then the class that represents our NPC’s would implement the interface and if later we decided that a light in our UI will start blinking when an enemy attacks then the class that has the light functionality would implement the IEnemyWarnings
interface.
With the help of a factory that creates instances of IEnemyWarnings
and by deriving from that interface, any changes to our program would become trivial. We would inject to the enemy class the appropriate implementation by getting it from the factory. Our interface is more closely coupled conceptually to the class that uses it, the Enemy
class, than the class that implements it every time. The name should represent that relationship and this will make any future changes easier by having the required class implement it.
Abstraction: What an Object Can Do vs What an Object Represents
The above example shows another difference between interfaces which are abstractions and classes that have implementations.
An interface shows us what a class can do, the implementation shows us what a class represents.
By keeping the above concept, certain problems are easier to solve. For example, let’s say that we have an inventory that can contain any number of items. Because there are a lot of items in our game, we also have containers that the player can have in his inventory and each container can also hold any number of items. Following that logic, inside each container we can have items or other containers that in turn can also have items or containers inside and so on.
First we create an interface that has all the functionality of an item. In this case to keep it simple, it has only a description:
public interface IItem
{
string Description { get; }
}
Then we create an Item
class that represents an item and obviously has all the functionality of an item, so it implements our interface:
public class Item : IItem
{
public string Description { get; }
public Item(string description) => Description = description;
}
Now we have to create a Container
class. This class can contain types that have the functionality that items have, so it derives from a collection that accepts IItem
, for example a List, but also has the functionality that items have, so it also implements the IItem
interface:
public class Container : List<IItem>, IItem
{
public string Description { get; }
public Container(string description) => Description = description;
}
Now our Container can contain IItems
, but is also of type IItem
itself, so it can be placed inside other containers. That means that if we have a player that has an inventory, which is of type Container
:
public class Player
{
public Container Inventory { get; } = new("The player inventory");
private string _indent = "";
public void ShowAllInventoryItems() => ShowContainerItems(Inventory);
private void ShowContainerItems(Container container)
{
foreach (IItem item in container)
{
Console.Write($"{_indent}{item.Description}");
if (item is Container { Count: > 0 } nestedContainer)
{
_indent += " ";
Console.WriteLine(" that contains: ");
ShowContainerItems(nestedContainer);
_indent = _indent.Remove(_indent.Length - 3, 3);
}
else
{
Console.WriteLine();
}
}
}
}
We can do:
Player player = new Player();
IItem stone = new Item("Small stone");
Container bagOfGuns = new Container("Bag of Guns");
Container gun = new Container("Small gun");
Container machineGun = new Container("Machine gun");
Container bagOfAmmo = new Container("Ammo bag");
IItem gunBullets = new Item("Some gun Bullets");
IItem machineGunBullets = new Item("Some machine gun Bullets");
player.Inventory.Add(stone);
player.Inventory.Add(bagOfGuns);
bagOfGuns.Add(gun);
bagOfGuns.Add(machineGun);
bagOfGuns.Add(bagOfAmmo);
bagOfAmmo.Add(gunBullets);
bagOfAmmo.Add(machineGunBullets);
We have added in the inventory a stone and a bag of guns. Inside the bag of guns, we have a small gun, a machine gun and a bag of ammo, inside the bag of ammo we have gun bullets and machine gun bullets.
By calling the player.ShowAllInventoryItems()
method we can see:
Small stone
Bag of Guns that contains:
Small gun
Machine gun
Ammo bag that contains:
Some gun Bullets
Some machine gun Bullets
We can now easily remove our gun bullets from our bug of ammo and add them inside our gun, because the gun is also a container:
bagOfAmmo.Remove(gunBullets);
gun.Add(gunBullets);
By calling the player.ShowAllInventoryItems()
method again we can see our change:
Small stone
Bag of Guns that contains:
Small gun that contains:
Some gun Bullets
Machine gun
Ammo bag that contains:
Some machine gun Bullets
Obviously that is not enough for an inventory system, as we need checks for what items we can add to certain containers, but that was not the intention. With this example I hope someone can see the difference between implementations and abstractions.
Our Items class is generic here, but in a game an item could be anything. For example an apple would be represented with an Apple
class that would implement the IItem
interface as well some other interfaces (ex. IConsumable
), our bullets would probably have their own implementations but would also implement the IItem
interface and so on.
Abstractions define what something can do, in this case both items and containers can do things that the items can do: Have a description.
In contrast, implementations define what a class represents: Our Item
class represents items and our Container
class represents containers that can contain items but also have the functionality of items. Containers though, are not items, but have the functionality that is being defined in the IItem
interface, which is a functionality our items should have, so they can be used as items by our systems.
The Water container example
Here is another example: In a game, a bucket is an item that has the functionality of the IItem
interface, but can also hold water. Instead of making the functionality of holding water as part of the bucket implementation, for example add water, remove water and get max water amount, we could create an IWaterContainer
interface.
With that interface, any system that is responsible for using the bucket to do something with the water it has, wouldn’t have to know anything about the bucket, but would only know about the IWaterContainer
interface.
This would allow us to make changes easier in the future. For example, a blanket in our game could also implement the IWaterContainer
interface, so that it could hold a small amount of water that represents that it is wet. This blanket would be usable from the system that is responsible for using the water an item has, as our system now does not depend on implementations, but only cares for the IWaterContainer
interface, which defines what an object can do and not what it represents.
Conclusion
Interfaces are the equivalent of pure abstract classes in other languages. They are abstractions that conceptually belong to the class or system that uses them and not to the classes that implements them. Their purpose is to define what a class can do, in contrast with concrete implementations that define what a class represents from the real world.
With that, let’s continue in the next post with the Interface segregation principle. Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.