Dependency Inversion Principle:
The Dependency Inversion Principle is perhaps the most complex of the SOLID principles. It contains several subprinciples and concepts, which I will explain below:
The first concept to understand is high-level vs low-level functionality. High-level functionality is functionality that is “close” to an Input or Output device. In a game development context, this could be menus that are shown to the player or inputs taken in from the player’s controller. Low-level functionality is functionality that is “far’ from an I/O device. This would include pulling data from databases and running operations on local player data.
The second, more complex subprinciple of the DIP is that you want your high-level classes to depend on interfaces rather than specific instances of classes.
As mentioned in the Interface Segregation Principle section, an interface is like a Scout Badge: it signifies a particular set of functionality. If a class implements an interface, it is given that interface’s methods.
For example, if an NPC class implements an ITrader interface, then it will have the ability to perform Trade functionality in addition to all the other things that an NPC can do.
Let’s continue with the Scout Badge metaphor to explain the concept of Dependency Injection, which is the third concept wrapped up within DIP. Let’s say that a Cantina class needs a Camper class with a Culinary interface to work in the Cantina’s kitchen. Dependency Injection is the practice of that Cantina asking at Awake() or within its constructor for a Camper with an ICulinary interface as opposed to a specific Camper with an ICulinary interface.
It’s the difference between, “Get me Seth, who knows how to cook” VS “Get me somebody who knows how to cook.”
Why does this matter?
With this structure, the Cantina does not need to rewrite its code extensively for it to change the Camper with an ICulinary interface that it will be using. In other words, the Cantina’s kitchen keeps running even when Seth is out sick.
Here is that same example written out in code:
public class Cantina : MonoBehavior {
public CantinaSettings myCantinaSettings;
private ICulinaryTrained myCulinaryTrained;
void Awake() {
myCulinaryTrained = myCantinaSettings.camperWithCulinaryTraining;
}
}
public class JuniorCamper : ICulinaryTrained {
@Overriding the CookFood method from ICulinaryTrained.
public void CookFood () {
MakeSmores();
}
…
}
public class SeniorCamper : ICulinaryTrained {
@Overriding the CookFood method from ICulinaryTrained.
public void CookFood () {
MakeBeans();
}
…
}
Given how we wrote the three classes above, the Cantina can freely switch between types of campers with Culinary training without rewriting almost any code. You’ll notice that SeniorCamper and JuniorCamper have different overrides of the CookFood method from ICulinaryTrained. What this means is the Cantina can choose between the two kinds of campers and their respective interpretations of food while ensuring it is getting some kind of food.