Skip to content

Instantly share code, notes, and snippets.

@HarryAmmon
Last active November 7, 2019 16:59
Show Gist options
  • Save HarryAmmon/ae20509753404905b1fb68eafdb44899 to your computer and use it in GitHub Desktop.
Save HarryAmmon/ae20509753404905b1fb68eafdb44899 to your computer and use it in GitHub Desktop.

SOLID Principles

SOLID is a mnemonic acronym used in Object-Orientated Programming for five design principles. Incorporating SOLID into your project should make your code more flexible, maintainable and understandable. SOLID stands for:

  • SRP - Single Responsibility Principle
  • OCP - Open Closed Principle
  • LSP - Liskov Substitution Principle
  • ISP - Interface Segregation Principle
  • DIP - Dependency Inversion Principle

Single Responsibility Principle

The Single Responsibility Principle states that a class or method should only be responsible for one task. This also means that a developer should only have one reason to change the behavior of a method or class. When a class only does one thing it mitigates against any unwanted side effects of changing the behavior of the class.

To see if you are breaking the Single Responsibility Principle, ask yourself what is the role of your class. If your explanation involves the word 'and' you are probably breaking the Single Responsibility Principle.

The responsibility of the following class currently is to calculate the annual pay and hourly pay of a permanent employee.

public class PermanentEmployeeServices
    {
        public decimal CalculateAnnualPay(decimal AnnualSalary, decimal AnnualBonus)
        {
            decimal annualPay = AnnualSalary + AnnualBonus;

            return annualPay;
        }

        public decimal CalculateHourlyPay(decimal AnnualSalary)
        {
            decimal hourlyPay = ((AnnualSalary / 52) / 5) / 7;

            return hourlyPay;
        }
    }

Also there are now two reasons to change the behavior of this class, if we want to change the behavior of how we calculate annual pay and if we want to change the behavior of how we calculate hourly pay.

This class should be separated into two classes. The first class will have the sole responsibility for calculating annual pay for an employee. The second will have the sole responsibility of calculating hourly pay.

public class AnnualPay
  {
    public decimal Calculate(decimal AnnualSalary, decimal AnnualBonus)
    {
      decimal annualPay = AnnualSalary + AnnualBonus;

      return annualPay;
    }
  }
public class HourlyPay
{
  public decimal Calculate(decimal AnnualSalary)
  {
    decimal hourlyPay = ((AnnualSalary / 52) / 5) / 7;

    return hourlyPay;
  }
}  

Now when discussing the role of the AnnualPay class it would be fair to say it is responsible for calculating the annual pay. It now has only one responsibility so conforms to the Single Responsibility Principle.

Open - Closed Principle

The Open - Closed Principle states that "software entities should be open for extension but closed for modification."

This means that when adding new functionality, the behavior of an existing method should not be changed as this could effect the behavior of other aspects of the code that expect the method to work in a certain way.

To add the new functionality the new method should be able to be extended. Being able to extend code means that the functionality of legacy systems is not effected whilst providing new functionality where required.

Using the current example of the HourlyPay method shown above lets pretend that we need to change the method to factor in an 8.5 hr working day instead of a 7 hr working day. This is simple to do for this method but should be avoided.

Instead we first need to change the method so it can be extended. In C# this can be done with the virtual reserved word.

public class HourlyPay
{
  public virtual decimal Calculate(decimal AnnualSalary)
    {
        decimal hourlyPay = ((AnnualSalary / 52) / 5) / 7;

        return hourlyPay;
    }
}     

We now need to create a child class that inherits from HourlyPay. In this class we can create the new functionality without impacting anything that may rely on the base class.

public class BetterPermanentEmployeeServices: PermanentEmployeeServices
    {
        public override decimal CalculateHourlyPay(decimal AnnualSalary)
        {
            decimal hourlyPay = (((AnnualSalary / 52) / 5) / 8.5m);

            return hourlyPay;
        }
    }

Loskiv Substitution Principle

The Loskiv Substitution Principle states that objects of the superclass should be replaceable by objects of the subclass. This means that all objects of the subclass will need to behave in the same way as the objects of its superclass. To do this overridden methods of the subclass shall accept the same parameter values as the method from the superclass. To achieve this data validation performed by the subclass cannot be more restrictive than the data validation performed by the superclass.

It can be easy to assume a type is a type of something in the real world but translating this into code can lead to issues.

Example

Your organization has two different types of employee, permanent employees and contracted/ temporary employees. Both of these types of employee share similar properties:

  • name,
  • employee ID,
  • start date,
  • end date.

These shared properties might encourage you to create a superclass called employee that both the tempEmployee and permanentEmployee classes would inherit from. So far, so good.

Now we need to calculate the hourly pay for both types of employee. For the tempEmployee the hourly rate should be calculated by dividing their daily rate by the number of hours worked. For the permanentEmployee the hourly rate should be calculated by dividing the sum of their annual salary and annual bonus by the number of working hours in a year. In each class we create a CalculateHourlyPay method. But with the tempEmployee class we require the parameter dailyRate and with the permanentEmployee class we require both annualSalary and annualBonus as parameters.

This is where the problem lies, when we are dealing with employee we cannot switch to working with the type permanentEmployee or tempEmployee because of the different parameters required by each types CalculateHourlyPay method. This breaks the Loskiv Substitution Principle.

The solve this it should be considered that if a permanent employee really is a employee. Just because they share some properties does not mean a 'is-a' relationship exists between them.

When creating data models using inheritance is appropriate when they share properties. When writing classes inheritance should be used where behavior is shared.

Interface Segregation Principle

The Interface Segregation aims to solve the problem of having fat interfaces or polluted interfaces. A fat or polluted interface is an interface that forces its clients to provide implementations for methods that they do not need. A common symptom of this is having client code that throws a NotImplemented exception message. To solve this the fat interface should be broken down into separate interfaces that contain less method definitions.

Below is a repository interface for completing CRUD operations for a permanent employee.

public interface IPermanentRepo
    {
        PermanentEmployee CreatePermanentEmployee(PermanentEmployee employee);

        List<PermanentEmployee> ReadPermanentEmployee(string Name);

        List<PermanentEmployee> ReadAllPermanentEmployees();

        bool DeletePermanentEmployee(PermanentEmployee employee);

        bool UpdatePermanentEmployee(PermanentEmployee employee, string field, string value);

        bool CheckPermanentEmployeeExists(string Name, out PermanentEmployee employee);
    }

When using the interface the client code is required to provide method definitions for all 4 CRUD operations. What if you wanted a repository that could only read permanent employees? You would have to use this interface and use the NotImplemented exception. This is now a fat interface. To solve this we should split up this interface into multiple, smaller interfaces.

public interface ICreatePermanent
    {
        PermanentEmployee CreatePermanentEmployee(PermanentEmployee employee);
    }
public interface IReadPermanent
    {
        List<PermanentEmployee> ReadPermanentEmployee(string Name);

        List<PermanentEmployee> ReadAllPermanentEmployees();
    }
public interface IUpdatePermanent
    {
        bool UpdatePermanentEmployee(PermanentEmployee employee, string field, string value);
    }
public interface IDeletePermanent
    {
        bool DeletePermanentEmployee(PermanentEmployee employee);
    }

Now a repository can have more granular control over what methods they have to provide a method implementation for and adheres to the Interface Segregation Principle.

But what if we want to have a repository that uses all 4 of the CRUD operations. For this there are two options. We could simply inherit every interface individually but this can look messy and could cause confusion. Instead we can user interface inheritance to create a new interface that inherits from the individual interfaces. We then inherit this new interface!

public interface IPermanentRepo: ICreatePermanent, IReadPermanent, IUpdatePermanent, IDeletePermanent

Dependency Inversion Principle

The aim of the Dependency Inversion Principle is to ensure that software modules are loosely coupled. High level modules should be independent of low level modules. I.e. your business logic should not care what file system you are using.

The principle states that:

  • High-level modules should not depend on the low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

This principle can be followed by using interfaces. A class should implement an interface. When an instance of this class is required or being referenced, the interface should be referenced instead. This ensures that the class referencing the interface does not depend on the details of how that interface is implemented. It also makes it easy to use a different class in your code, as long as it implements the same interface then the client code will not notice that a different implementation is being used.

This helps with implementing Dependency Injection. Any dependencies that a class has should be defined in the constructor of a class. The constructor should require an object that implements an interface as a parameter. This is called constructor dependency injection. This loose coupling also makes unit tests easier to write as interfaces are easier to mock.

Using Dependency Inversion allows you to use the dependency injection containers provided by .NETCore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment