SOLID Principles for better Software-Architecture

SOLID is a acronym five fundamentals of software design which will improve the readability, maintainability and the extensibility of your code:

– Single Responsibility
– Open-Closed
– Liskov Substitution
 Interface Segregation
– Dependency Inversion

In this Blog-Post I want to give you an overview of the five SOLID – Principles and in the next two Blog-Posts I will dive deeper into the two most important Principles:
Single Responsibility and Dependency Inversion.

Just click the links at the top to have a look at the respective blog-post.

Another note: I will talk about each principle from most important to least important.
Another another note: Softwarearchitecture tends to be a very religous topic and I have no intent on ending up as a heretic burning on a stake because I offended proponents of diverging programming-styles. So believe me when I say that these Blog-Posts of course reflect my humble opinion and they certainly are not truth incarnate but rather my take on explaining the SOLID Principles to Junior Programmers.

1. Single Responsibility Principle

This is an important one!
Each system,class and function should be responsible for exactly one thing.

In practice this means, that you player-class shouldn’t handle input, movement, UI, pathfinding, sound etc. in one single script. Instead you should split the player-class into smaller pieces which are responsible for one particular thing.
This is one of the most important principles of Software-Architecture so I highly suggest you read my blog-post where I dive deeper into this principle.

Single Responsibility – Principle in Action: We have a PlayerController which has References to 3 other scripts which handle different aspects of our player

2. Dependency Inversion

This principle is also extremely important for high quality code.

High-Level classes (i.e.PlayerController) should not be directly dependant on specific Low-Level classes (i.e. EnemyBullet, EnemyRocket, EnemyLaser, …). Instead these High-Level Classes should be dependant on abstractions of these classes i.e. an Interface IProjectile.

So in a nutshell:
Use Interfaces to reduce dependency on specific classes.
This concept is hard to grasp without an example so you may want to read this blog-post where I dive deeper into this principle.

3. Liskov Substitution

This principle basically just says, that if you have two Subclasses of a baseclass, they should work for all the functions and variables of the baseclass.

Here’s an example of what you should not do:

public interface IItem
{
    string _Name { get; }
    int _Price { get; }
    int _Charges { get; }
    void Equip();
    void Use();
}

public class HealPotion : IItem
{
    public Player _player;
    
    public string _Name { get; }
    public int _Price { get; }
    public int _Charges { get; private set;}
    
    public void Equip()
    { }

    public void Use()
    {
        _player.health += 10;
        _Charges--;
    }
}

The Problem here is, that IItem forces the HealPotion to have the Equip-Function which doesn’t make any sense for the potion because it is just a consumable that can not be equipped. Also if you have an armorpiece it must implement the Use-Function and the Charges-Property which would make no sense for Armor to have.

So a lazy programmer might just leave the unused Functions of the interface empty which works for now but might lead to tremendous Bug-Potential down the road when you start implementing complex inventory functionality.

So what the Liskov substitution Principle says is, that you must not leave these functions empty but adapt your code so it makes more sense (You can find the solution to this problem in next Principle: Interface Segretation)

4. Interface Segregation

The Solution to the problem that I explained in the Liskov Substitution is to keep your interfaces small. i.e. Instead of having a single IItem – Interface which handles everything an Item could possibly do, we use seperate Interfaces for each functionality.
That way we can make sure a potion can be used but not equipped:

public interface IItem
{
    string _Name { get; }
    int _Price { get; }
}

public interface IConsumable
{
    int _Charges { get; }
    void Use();
}

public interface IEquippable
{
    void Equip();
}

public class HealPotion : IItem, IConsumable
{
    public Player _player;
    
    public string _Name { get; }
    public int _Price { get; }
    public int _Charges { get; private set;}

    public void Use()
    {
        _player.health += 10;
        _Charges--;
    }
}

5. Open – Closed Principle

Open-Closed Principle means that, classes should be open for extension and closed for modification.

Unfortunately this definition is slightly confusing because it is not clear what “closed for modification” means at first. So I would rephrase it like this:

Your class should be open for extension without having to modify the original code.
While this principle is certainly something to keep in the back of your head, I think it overlaps mostly with the dependency inversion Principle.