SOLID Principles cheat sheet in C#

A summary of SOLID Principles as implemented in the C# language.

Useful link: https://code-maze.com/solid-principles/

1. Single Responsibility Principle

Every class should have only one responsibility. Consider the class PersonalDetails whose responsibility is to hold details about a person:

public class PersonDetails
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Identifier { get; set; }

    public void PrintReport()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age.ToString());
        Console.WriteLine("Identifier: " + Identifier);
    }
}

After instantiating, we then use a public method to print out the details of that person:

PersonDetails person1 = new PersonDetails
{
    Name = "Andrew",
    Age = 36,
    Identifier = "MDJW0031"
};

person1.PrintReport();

The problem is that this class has more than one responsibility. It is not just holding details of a person but printing it too.
More potential complexities will ensue if it is decided we want to add other unrelated functionality such save the person to disk too.

We overcome this by separating unrelated code.

public class PersonDetails
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Identifier { get; set; }
}

And creating a new class to handle the responsibility of printing:

public class Printer
{
    public void Print(PersonDetails personDetails)
    {
        Console.WriteLine("Name: " + personDetails.Name);
        Console.WriteLine("Age: " + personDetails.Age.ToString());
        Console.WriteLine("Identifier: " + personDetails.Identifier);
    }
}

Implemented as follows:

Printer printer = new Printer();
printer.Print(new PersonDetails
{
    Name = "Andrew",
    Age = 36,
    Identifier = "MDJW0031"
});

2. Open-closed Principle

This pattern is useful for lowering the chance of bugs, and means “Open for extension, closed for modification”

In other words, it should be easy to extend the functionality of classes without having to modify them.

Consider the following example which deals with the filtering of computer monitors by type and screen:

public enum MonitorType
{
    OLED,
    LCD,
    LED
}
public enum Screen
{
    WideScreen,
    CurvedScreen
}
public class ComputerMonitor
{
    public string Name { get; set; }
    public MonitorType Type { get; set; }
    public Screen Screen { get; set; }
}
public class MonitorFilter
{
    public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, 
MonitorType type) =>
            monitors.Where(m => m.Type == type).ToList();
}
var monitors = new List<ComputerMonitor>
{
    new ComputerMonitor { Name = "Samsung S345", Screen = Screen.CurvedScreen, Type = MonitorType.OLED },
    new ComputerMonitor { Name = "Philips P532", Screen = Screen.WideScreen, Type = MonitorType.LCD },
    new ComputerMonitor { Name = "LG L888", Screen = Screen.WideScreen, Type = MonitorType.LED },
    new ComputerMonitor { Name = "Samsung S999", Screen = Screen.WideScreen, Type = MonitorType.OLED },
    new ComputerMonitor { Name = "Dell D2J47", Screen = Screen.CurvedScreen, Type = MonitorType.LCD }
};

var filter = new MonitorFilter();
var lcdMonitors = filter.FilterByType(monitors, MonitorType.LCD);
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
    Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}

Change MonitorFilter class:

public class MonitorFilter
{
    public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, MonitorType type) =>
        monitors.Where(m => m.Type == type).ToList();
    public List<ComputerMonitor> FilterByScreen(IEnumerable<ComputerMonitor> monitors, Screen screen) =>
        monitors.Where(m => m.Screen == screen).ToList();
}

This gives the desired result but violates the open-closed principle. The solution is to use interfaces:

public interface ISpecification<T>
{
    bool isSatisfied(T item);
}

public interface IFilter<T>
{
    List<T> Filter(IEnumerable<T> monitors, ISpecification<T> specification);
}

Create a separate class for the monitor type specification to implement ISpecification:

public class MonitorTypeSpecification : ISpecification<ComputerMonitor>
{
    private readonly MonitorType _type;
    public MonitorTypeSpecification(MonitorType type)
    {
        _type = type;
    }
    public bool isSatisfied(ComputerMonitor item) => item.Type == _type;
}

Modify the existing MonitorFilter class by getting it to implement the Ifilter interface:

public class MonitorFilter : IFilter<ComputerMonitor>
{
    public List<ComputerMonitor> Filter(IEnumerable<ComputerMonitor> monitors, ISpecification<ComputerMonitor> specification) =>
        monitors.Where(m => specification.isSatisfied(m)).ToList();
}

On implementing this in the program, the result should be the same:

var filter = new MonitorFilter();
var lcdMonitors = filter.Filter(monitors, new MonitorTypeSpecification(MonitorType.LCD));
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
    Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}

So it is now possible to make further extensions to our MonitoFilter class without actually modifiying the class itself. If we want to filter by screen type, create a new screen specification class that implements the Ispecification interface, and use that:

public class ScreenTypeSpecification : ISpecification<ComputerMonitor>
{
    private readonly Screen _screen;
    public ScreenTypeSpecification(Screen screen)
    {
        _screen = screen;
    }
    public bool isSatisfied(ComputerMonitor item) => item.Screen == _screen;
}

And make use of this in the main code:

var filter = new MonitorFilter();
var wideScreenMonitors = filter.Filter(monitors, new ScreenSpecification(Screen.WideScreen));
foreach (var monitor in wideScreenMonitors)
{
    Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}

3. Liskov Substitution Principle

A form of polymorphism. Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. Derived class objects should be able to replace base class objects without modifying it’s behaviour:

Example classes to calculate the sum of all numbers or just even numbers.

public class SumCalculator
{
    protected readonly int[] _numbers;
    public SumCalculator(int[] numbers)
    {
        _numbers = numbers;
    }
    public int Calculate() => _numbers.Sum();
}
public class EvenNumbersSumCalculator : SumCalculator
{
    public EvenNumbersSumCalculator(int[] numbers)
        : base(numbers)
    {
    }
    public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");

Works fine. But of we try this:

var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");

We don’t get the result we desire:

We don’t get the expected result because our variable evenSum is of type SumCalculator which is the base class. This means that the Count method from the SumCalculator class will be executed, not the EvenNumbersSumCalculator class. Therefore our child class is not behaving as a substitute for the parent class, thereby violating the Liskov substitution principle.

We overcome this by using abstract base classes:

public abstract class Calculator
{
    protected readonly int[] _numbers;
    public Calculator(int[] numbers)
    {
        _numbers = numbers;
    }
    public abstract int Calculate();
}
public class SumCalculator : Calculator
{
    public SumCalculator(int[] numbers) : base(numbers) { }
    public override int Calculate() => _numbers.Sum();
}
public class EvenNumbersSumCalculator : Calculator
{
    public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
    public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
Calculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
Calculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");

4. Interface-segregation principle

This states that no client should be forced to depend on methods it does not need or should not use . Many client-specific interfaces can work better than one general-purpose interface.

For example, to develop a behavior for a multifunctional car , there are vehicles that we can only drive and there are vehicles that we can only fly. But there are vehicles we can both drive and fly. We would like to create a class which supports all these actions for a vehicle.

public interface IVehicle
{
    void Drive();
    void Fly();
}

 

public class MultiFunctionalCar : IVehicle
{
    public void Drive()
    {
        Console.WriteLine("Drive a multifunctional car");
    }
    public void Fly()
    {
        Console.WriteLine("Fly a multifunctional car");
    }
}

This covers the requirements for a multi-functional car. However if we want to develop classes for drive-only and fly-only vehicles, we see inefficiencies:

public class Car : IVehicle
{
    public void Drive()
    {
        Console.WriteLine("Driving a car");
    }
    public void Fly()
    {
        throw new NotImplementedException();
    }
}
public class Airplane : IVehicle
{
    public void Drive()
    {
        throw new NotImplementedException();
    }
    public void Fly()
    {
        Console.WriteLine("Flying a plane");
    }
}

Each class only contains one required declaration: the unwanted one is made to throw an exception, which is wasteful.

The solution is to split the IVehicle interface into separate Icar and IAircraft interfaces:

public interface ICar
{
    void Drive();
}
public interface IAirplane
{
    void Fly();
}

Resulting in classes that only implement the methods they need:

public class Car : ICar
{
    public void Drive()
    {
        Console.WriteLine("Driving a car");
    }
}
public class Airplane : IAirplane
{
    public void Fly()
    {
        Console.WriteLine("Flying a plane");
    }
}
public class MultiFunctionalCar : ICar, IAirplane
{
    public void Drive()
    {
        Console.WriteLine("Drive a multifunctional car");
    }
    public void Fly()
    {
        Console.WriteLine("Fly a multifunctional car");
    }
}

5. Dependency Inversion Principle

These depend upon abstractions, [not] concretions

Dependency Inversion Principle is a means of creating logic for higher-level modules in such a way to be reusable and unaffected by any changes from the lower level modules in the application.

Having this idea in mind the Dependency Inversion Principle states that

  • High-level modules should not depend on low-level modules, both should depend on abstractions.
    Abstractions should not depend on details. Details should depend on abstractions.

Consider an example violation of the dependency inversion principle, a low-level class used to keep track of an employee’s details:

public enum Gender
{
    Male,
    Female
}

public enum Position
{
    Administrator,
    Manager,
    Executive
}

public class Employee
{
    public string Name { get; set; }
    public Gender Gender { get; set; }
    public Position Position { get; set; }
}

public class EmployeeManager
{
    private readonly List<Employee> _employees;
    public EmployeeManager()
    {
        _employees = new List<Employee>();
    }
    public void AddEmployee(Employee employee)
    {
        _employees.Add(employee);
    }
}

Which is used by a highler-level class used to do statistical analysis on the employees:

public class EmployeeStatistics
{
    private readonly EmployeeManager _empManager;
    public EmployeeStatistics(EmployeeManager empManager)
    {
        _empManager = empManager;
    }
    public int CountFemaleManagers()
    {
        //logic goes here
    }
}

We cannot make use of the private _employees list in the EmployeeManager class, so one solution would be expose the private list:

Then we are in a position to count the employees:

public class EmployeeStatistics
{
    private readonly EmployeeManager _empManager;
    public EmployeeStatistics(EmployeeManager empManager)
    {
        _empManager = empManager;
    }
    public int CountFemaleManagers() =>
      _empManager.Employees.Count(emp => emp.Gender == Gender.Female && emp.Position == Position.Manager);
}

This works, but is not good practice, given these two problems:

We can’t send any other object in the EmployeeStatistics constructor except the EmployeeManager object.
We are using the public property from the low-level class inside the high-level class. In doing so, our low-level class can’t change its way of keeping track of employees, without it impacting the higher-level class. For example, if we want to implement it using a dictionary instead of a list, we would also need to change the EmployeeStatistics higher-level class.

The solution is to decouple our two classes so the both of them depend on abstraction.

public interface IEmployeeSearchable
{
    IEnumerable<Employee> GetEmployeesByGenderAndPosition(Gender gender, Position position);
}

public class EmployeeManager : IEmployeeSearchable
{
    private readonly List<Employee> _employees;
    public EmployeeManager()
    {
        _employees = new List<Employee>();
    }

    public void AddEmployee(Employee employee)
    {
        _employees.Add(employee);
    }
    public IEnumerable<Employee> GetEmployeesByGenderAndPosition(Gender gender, Position position)
        => _employees.Where(emp => emp.Gender == gender && emp.Position == position);
}

Then modify the higher-level EmployeeStatistics class, so that it is no longer dependant on the lower-level EmployeeManager class, but uses the interface in the constructor instead.

public class EmployeeStatistics
{
    private readonly IEmployeeSearchable _emp;
    public EmployeeStatistics(IEmployeeSearchable emp)
    {
        _emp = emp;
    }
    public int CountFemaleManagers() =>
    _emp.GetEmployeesByGenderAndPosition(Gender.Female, Position.Manager).Count();
}

It now becomes possible to modify EmployeeManager without us having to worry about the EmployeeStatistivs class as well. They are decoupled.

Check the result:

var empManager = new EmployeeManager();
empManager.AddEmployee(new Employee { Name = "Leen", Gender = Gender.Female, Position = Position.Manager });
empManager.AddEmployee(new Employee { Name = "Mike", Gender = Gender.Male, Position = Position.Administrator });
var stats = new EmployeeStatistics(empManager);
Console.WriteLine($"Number of female managers in our company is: {stats.CountFemaleManagers()}");

`