Back to basics #2 – SOLID design principles

We have another acronym! Today I will be writing about the SOLID software programming principles and hopefully giving some useful code examples. I was trying to get my head around these principles for a while and a lot of the examples I found didn’t really make sense to me (circles, squares and cuboids featured heavily) so I’m going to use the context of a fictional restaurant as I think just about everyone can relate to a restaurant. All the code samples below can be found on my GitHub Design Patterns repo – please bear in mind that it is a work in progress!

The principles were introduced by Robert C. Martin* back in 2000 in his paper ‘Design Principles and Design Patterns‘. If you want to have a bit of a deep dive you can find the paper here (warning – it’s 34 pages long, but it is an interesting read). The SOLID acronym itself was coined in 2004 by Michael Feathers.

Essentially, they are a set of rules and best practices to follow when working with Object Oriented design. The principles are:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov-Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

*aka “Uncle Bob” – incidentally, I can highly recommend his book “The Clean Coder” if you want to get an idea of what it’s like to work as a developer and how you can develop the non-technical skills you’ll be needing.


Single Responsibility Principle

An object should do one thing and have only one reason to change.

This means that a class should only have one responsibility (i.e. one job to do).

Our restaurant has asked us to develop a way for them to generate and print receipts. At first we might create a ReceiptService class to handle this:

// This is bad code - do not use it!
public class ReceiptService
{
    public void GenerateReceipt()
    {
        // Generate the customer's receipt
    }

    public void PrintReceipt()
    {
        // Print the customer's receipt
    }
}

This violates the Single Responsibility Principle straight away because the class has multiple responsibilities – it generates a receipt and it prints a receipt. They are entirely unrelated: the GenerateReceipt method does not need to know anything about the PrintReceipt method and vice versa.

To rectify this we can create two separate classes – a ReceiptGenerator and a ReceiptPrinter.

public class ReceiptGenerator
{
    public void GenerateReceipt()
    {
        // Generate the receipt
    }
}

public class ReceiptPrinter
{
    public void PrintReceipt()
    {
        // Print the receipt
    }
}

Both the classes now handle their own responsibility. It is important to note that this does not mean that a class cannot have more than one method – it can have as many methods as is needed, but they must all be relevant to the class’ responsibility.

The benefits of the single responsibility principle are that the code has better readability, it is easier to debug and test and it has better maintainability.


Open-Closed Principle

A class should be open for extension and closed for modification.

Basically, a class should not need to be changed if new functionality is added to an existing method. For this example, our restaurant has asked us to allow certain customers/staff discounts off their bill. The amount depends on the type of customer.

Sounds simple, right? A simple if statement will handle it. But wait – this violates the open closed principle because if we add another discount (e.g. manager) we will need to modify this class to accommodate the new type by adding another if statement. And then another, and another, and before we know it we have ten different types of customer discount and our code is difficult to maintain and test. Each time you modify a method you run the risk of altering the behaviour of the code and potentially breaking other parts of your system that rely on it.

// This is bad code - do not use it!
public class Customer
{
    public string Name { get; set; }
    public DateTime DoB { get; set; }
    public string Type { get; set; }

    public double CalculateDiscountedBill(double billAmount)
    {
        if (this.Type == "Student")
        {
            return billAmount * 0.9;
        }
        if (this.Type == "Staff")
        {
            return billAmount * 0.95;
        }
        return billAmount;
    }
}

We can overcome this by using inheritance. Here we are creating an abstract base class which contains the CalculateDiscountedBill method. We then have separate classes for each type of customer/staff which inherit from the base class and are able to override the abstract method and implement their own logic (in this case, how much discount should be applied to the bill).

public abstract class CustomerBase
{
    public abstract double CalculateDiscountedBill(double billAmount);
}

public class StandardCustomer : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 1;

    public override double CalculateDiscountedBill(double billAmount)
    {
        return billAmount * DiscountMultiplier;
    }
}

public class StaffCustomer : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 0.05;

    public override double CalculateDiscountedBill(double billAmount)
    {
        var discount = billAmount * DiscountMultiplier;
        return billAmount - discount;
    }
}

public class StudentCustomer : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 0.1;

    public override double CalculateDiscountedBill(double billAmount)
    {
        var discount = billAmount * DiscountMultiplier;
        return billAmount - discount;
    }
}

Now if we need to add another type of discount we can simply create a new class, inherit from the CustomerBase class and override the CalculateDiscountedBill method with whatever logic is required.


Liskov-Substitution Principle

Objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.

For this example I’m going to expand on the code we ended up with in the Open-Closed Principle above. The restaurant wants to give its managers double the discount that the non-managerial staff are given. This will cause a problem with our code as we already have a StaffCustomer class which the manager will come under, so we would need to modify the CustomerBase to allow us to determine whether the staff member is a manager or not (e.g. a job title). But if we do that, the StudentCustomer and StandardCustomer classes will no longer be able to inherit from the CustomerBase class as they don’t require the job title property.

To solve this problem we can add a new class called StaffManager which inherits from the StaffCustomer class (which in turn inherits from the CustomerBase). The StaffManager now has all the properties and functionality of the StaffCustomer class but we can now add to it to allow the double discount.

public abstract class CustomerBase
{
    public abstract double CalculateDiscountedBill(double billAmount);
}

public class StandardCustomer : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 1;

    public override double CalculateDiscountedBill(double billAmount)
    {
        return billAmount * DiscountMultiplier;
    }
}

public class Staff : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 0.05;

    public override double CalculateDiscountedBill(double billAmount)
    {
        var discount = billAmount * DiscountMultiplier;
        return billAmount - discount;
    }
}

public class StaffManager : Staff
{
    public override double CalculateDiscountedBill(double billAmount)
    {
        var discount = billAmount * (DiscountMultiplier * 2);
        return billAmount - discount;
    }
}

public class Student : CustomerBase
{
    public double DiscountMultiplier { get; set; } = 0.1;

    public override double CalculateDiscountedBill(double billAmount)
    {
        return billAmount * DiscountMultiplier;
    }
}

This principle ensures the inheritance hierarchy is correctly designed and implemented and allows the architecture to be flexible. It helps prevent model hierarchies that violate the open closed principle – any inheritance model that follows the Liskov Substitution Principle will implicitly adhere to the Open Closed Principle.


Interface Segregation Principle

No client should be forced to depend on interfaces they don’t use.

This is only achievable if your interfaces fit a specific client or task so we split them down into multiple, independent parts.

In this example we have an interface that defines the duties of a staff member. In addition to the basic duties the managers also handle complaints (remember that double discount?). The problem with this is that any class that implements this interface must implement the HandleComplaint() method. This violates the Interface Segregation Principle because the waiting staff do not use that method.

// This is bad code - do not use it!
public interface IStaffDuties
{
    void ServeFood();
    void CleanTable();
    void WashUp();
    void HandleComplaint();
}

public class StaffService : IStaffDuties
{
    public void ServeFood()
    {
        // Serve the food
    }

    public void CleanTable()
    {
        // Clean the table
    }

    public void WashUp()
    {
        // Wash the pots
    }

    // This only applies to managers!
    public void HandleComplaint()
    {
        // Handle the complaint
    }
}

To solve this we separate this into two interfaces: IStaffDuties and IStaffManagerDuties. We then have two separate services, one for the waiting staff and one for the managers. As you can see from the code below, the staff manager service implements both the interfaces, giving the class access to all the methods needed. The waiting staff class only implements the IStaffDuties interface so only has access to the methods it needs.

public interface IStaffDuties
{
    void ServeFood();
    void CleanTable();
    void WashUp();
}

public interface IStaffManagerDuties
{
    void HandleComplaint();
}

public class WaitingStaffService : IStaffDuties
{
    public void ServeFood()
    {
        // Serve the food
    }

    public void CleanTable()
    {
        // Clean the table
    }

    public void WashUp()
    {
        // Wash the pots
    }
}

public class StaffManagerService : IStaffDuties, IStaffManagerDuties
{ 
    public void ServeFood()
    {
        // Serve the food
    }

    public void CleanTable()
    {
        // Clean the table
    }

    public void WashUp()
    {
        // Wash the pots
    }

    public void HandleComplaint()
    {
        // Handle the complaint
    }
}

Applying this principle to our code design reduces the side effects and frequency of required changes, making the code much more maintainable.


Dependency Inversion Principle

A high-level class must not depend upon a lower-level class. They must both depend upon abstractions.

The premise behind this principle is that different classes don’t need to know the actual implementation of code in other classes – all a class needs to know is that the other classes exist and that they provide methods that will do tasks. They don’t need to know how the tasks are done, just that they get done. For example, I use a kettle to boil water for a cup of tea. I don’t need to know how the electricity is generated and provided to the socket on my kitchen wall, I don’t need to know how the kettle heats the water – I just need to know that I can call the BoilWater() method (i.e. press a switch on the kettle) and the water is magically heated to boiling temperature.

In this example, our restaurant needs to calculate staff salary based on their hourly wage and the number of hours worked. The code below violates the Dependency Inversion Principle because the StaffRecord class directly depends on the SalaryCalculator class.

// This is bad code - do not use it!
public class SalaryCalculator
{
    public double CalculateSalary(int hoursWorked, double hourlyWage) => hoursWorked * hourlyWage;
}

public class StaffRecord
{
    public int HoursWorked { get; set; }
    public double HourlyWage { get; set; }
    public double GetSalary()
    {
        var salaryCalculator = new SalaryCalculator();
        return salaryCalculator.CalculateSalary(HoursWorked, HourlyWage);
    }
}

We can use interfaces and dependency injection to ensure our code does not violate this principle. Here we use an ISalaryCalculator interface to allow decoupling between the SalaryCalculator and StaffRecord classes. The StaffRecord class no longer directly depends on the SalaryCalculator.

To allow the StaffRecord class access to the ISalaryCalculator we use dependency injection in the constructor of the class (which is a completely different topic! It’s on the list of things to write about so I’m not going to go into it here, but it’s important you know that it’s used here).

public interface ISalaryCalculator
{
    double CalculateSalary(int hoursWorked, double hourlyWage);
}

public class SalaryCalculator : ISalaryCalculator
{
    public double CalculateSalary(int hoursWorked, double hourlyWage) => hoursWorked * hourlyWage;
}

public class StaffRecord
{
    private readonly ISalaryCalculator _salaryCalculator;

    public int HoursWorked { get; set; }
    public double HourlyWage { get; set; }

    // Using Dependency Injection in the constructor of the class.
    public StaffRecord(ISalaryCalculator salaryCalculator)
    {
        _salaryCalculator = salaryCalculator;
    }

    public double GetSalary()
    {
        return _salaryCalculator.CalculateSalary(HoursWorked, HourlyWage);
    }
}

Decoupling classes like this makes the code much more maintainable and expandable. It means that we can make changes to the lower level classes more easily as the changes will not affect the higher level class.


Summary

Gosh that was a long one! Thanks for sticking with me, I hope this has helped shed a bit more light on the SOLID principles and how they are applied in the real world. Following these will guide you to a more flexible, maintainable and robust system design. I’d love to hear your thoughts on the principles and how you find implementing them – comment below!


Discover more from Diffident Coder

Subscribe to get the latest posts sent to your email.

Leave a comment