Certificates, Games, Programming

I Completed a Course on SOLID Principles!

If you’re not familiar with the SOLID principles, they are a set of principles that software engineers use to futureproof their code. They don’t necessarily make writing your code the first time easier, but they do make all subsequent modifications easier and less bug-prone.

Robert Nystrom, in his book Game Programming Patterns, states that your codebase should be like a calm pond - it shouldn’t ripple when you touch it. The SOLID principles are in pursuit of this same goal: to minimize the bugs and rewriting that occurs when new features or modifications are made to an existing codebase.

Here are the five principles with a brief explanation of each:

Single Responsibility Principle: every software component should have only one purpose and/or one reason to change.

Why? Let’s say we to want to create a game in which a Farmer class Digs up Vegetable objects (capitalization used here to designate classes and methods). We also want to create a Scavenger class that can Dig up wild Vegetables.

The incorrect approach would be to forego writing a Shovel class and instead write our Farmer and Scavenger classes so that they have the ability to Dig inherently. Our Farmer and Scavenger classes are now responsible for everything that Farmers and Scavengers do respectively plus Digging, so we are in violation of the principle.

But what if we have to change how Digging works in our game? In that case, we now need to change the Dig function in both our Farmer and Scavenger classes. Because we’re changing two classes instead of one, there is more work for us to do and we’re more likely to leave bugs in our wake.

If we were following the SRP, we would instead have separate Farmer, Scavenger, Shovel, and Vegetable classes; our Farmer and Scavenger classes would call on the Shovel class to Dig Vegetables. With this approach, if we need to update our Digging, we only need to update our Shovel class.


Open-Closed Principle: You should be able to add features to your codebase without modifying existing code.

One basic way you can follow this principle is by using generic classes that you believe will encapsulate objects that will be added to your game later on. To continue our Farming game example above, let’s say you want your Farmer class to run an ExtractSeed function on Blueberry objects, but you expect that you will probably want the Farmer to run ExtractSeed on other fruits in the future.

Instead of setting the parameter of ExtractSeeds to Blueberry {ExtractSeeds(BlueBerry incomingBlueBerry)}, you can instead set the parameter to Fruit {ExtractSeeds(Fruit incomingFruit)}. This way, you can have the Farmer run ExtractSeeds on Watermelons, Strawberries, and Kiwis without having to update the ExtractSeeds method.


Liskov Substitution Principle:

You should be able to replace any class with any of its subtypes without breaking your codebase.

In plain English, this means all objects called Fruits should be Fruits, all things called Animals should be Animals, etc.

Why? Imagine our Farmer class has an ExtractSeeds method that takes in a Fruit class and returns Seeds. (All Fruits have seeds of some kind.) If Rhubarb is mistakenly labeled as a Fruit due to its sweet flavor, the game will break when the Farmer runs ExtractSeeds on the seedless Rhubarb.


Interface Segregation Principle:

Interfaces are like Scout Badges; they signify that someone has a specific skillset. For example, if a class implements a IFlier interface, you can assume that it can Fly.

The Interface Segregation Principle basically says that interfaces should be narrow enough that they only cover one set of tightly related functionality. So an IFlier interface should not also contain functions that relate to combat. The reason for segregating interfaces is straightforward: if we put Combat-related functions in our IFlier interface, we might end up with a Hummingbird class that has a Slash function, which is not appropriate.

Many languages allow classes to implement multiple interfaces, so you could have an Eagle class that implements both your ICombatant and IFlier interfaces.


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.


I love hearing new perspectives on these topics, so please leave some comments below!