Said's Blog

C# - SOLID principles

January 22, 20236 min read
image

SOLID is an acronym in object-oriented design, with each letter representing a design principle. Its aim is to reduce dependencies, so one area can be changed, without affecting others. In this blog, we will go through each one, find out what they are, and illustrate with an example.

Single-responsibility principle

This principle states that a class should have only one reason to change, which means every class should have a single responsibility.

Single Responsibility
Image Credit: medium.com

Let's apply this principle on the following example:

						public class Team 
{ 
}
public class TeamService {
    public void Create(Team team) { }
     
    public void Update(int id, Team team) { }
 
    public void Delete(int id) { }
 
    public void AddToCompetition(int teamId, int competitionId) { }
}
					

As the AddToCompetition method is related more to the competition than it is to the team, we should create a separate service for the competition methods. That means that the team and competition are separate from each other.

						public class Team 
{ 
}
public class TeamService {
    public void Create(Team team) { }
     
    public void Update(int id, Team team) { }
 
    public void Delete(int id) { }
}
public class CompetitionService {
    public void AddToCompetition(int teamId, int competitionId) { }
}
					

Open–closed principle

This principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification, which means we should be able to extend a class behavior, without modifying it.

Open–closed
Image Credit: medium.com

In the following example, there are two classes. One to represent a car and one to represent a motorbike. Both classes can specify the number of wheels that are attached to it.

						public class Car 
{
    public int Wheels { get; set; }
} 
public class Motorbike 
{
    public int Wheels { get; set; }
}
public class Wheel 
{
 
    public int CountTotalWheels(object[] vehicles) {
         
        var wheelCount = 0;
         
        foreach (var vehicle in vehicles) {        
            if (vehicle is Car) {
                wheelCount += ((Car)vehicle).Wheels;            
            }
            if (vehicle is Motorbike) {
                wheelCount += ((Motorbike)vehicle).Wheels;            
            }        
        }    
         
        return wheelCount;
    }
}
					

The CountTotalWheels method is not open for extension and can only handle cars and motorbikes without modification. In order to handle a new vehicle like a bus, the method needs to be modified. Hence, the open-closed principle is not met.

Here is how we can apply the open-closed principle:

						public abstract class Vehicle 
{
    public int Wheels { get; set; }
} 
public class Car : Vehicle 
{        
} 
public class Motorbike : Vehicle 
{    
} 
public class Wheel {
 
    public int CountTotalWheels(Vehicle[] vehicles) {
         
        var wheelCount = 0;
         
        foreach (var vehicle in vehicles) {    
            wheelCount += vehicle.Wheels;                    
        }    
         
        return wheelCount;
    }
}
					

By moving the Wheel property to a Vehicle base class, both the Car and Motorbike will be able to inherit it. And if we want to count the wheels of a different vehicle like a bus, all we will need to do is add a new class that inherits the Vehicle base class without modifying the CountTotalWheels method.

Liskov substitution principle

This principle ensures that any class that is the child of a parent class should be usable in place of its parent without any unexpected behavior.

Liskov Substitution
Image Credit: medium.com

Let's apply this principle on the following example:

						public abstract class Vehicle 
{
    public int Wheels { get; set; }
    public virtual string? HelmetDesign { get; set; }
}
public class Car : Vehicle 
{
    public override string? HelmetDesign => throw new NotImplementedException();
}
public class Motorbike : Vehicle 
{
}
					

Since helmet is not required when driving a car, the child Car class is breaking the parent Vehicle class because it's not returning a string for the HelmetDesign property. Therefore, it's violating the Liskov substitution principle.

We can can remove the HelmetDesign property out of the Vehicle class, create a new IHelmet interface and add the property into there. That means that the Motorbike class can implement IHelmet and include its members. As there is no requirement to wear a helmet in a car, the Car class just inherits the Vehicle class.

						public abstract class Vehicle 
{
    public int Wheels { get; set; }
}
public interface IHelmet 
{
    string? HelmetDesign { get; set; }
}
public class Car : Vehicle 
{
}
public class Motorbike : Vehicle, IHelmet 
{
    public string? HelmetDesign { get; set; }
}
					

Interface segregation principle

This principle states that a class implements an interface where it requires all its members.

Interface Segregation
Image Credit: medium.com

Let's apply this principle on the following example:

						public interface ITeam 
{    
    int BallCount { get; set; }    
    int WicketCount { get; set; }
}
public class CricketTeam : ITeam 
{
    public string? Name { get; set; }    
    public int BallCount { get; set; }
    public int WicketCount { get; set; }
}
public class PoolTeam : ITeam 
{
    public string? Name { get; set; }    
    public int BallCount { get; set; }
    public int WicketCount { get; set; }
}
					

This example violates interface segregation principle as the PoolTeam class is inheriting an interface where it doesn't require all the members (the WicketCount is not needed for PoolTeam class).

To fix that, we can have a separate interface for the wicket. That means that each class only includes the properties that it requires.

						public interface ITeam
{
    string? Name { get; set; }
    public int BallCount { get; set; }
}
public interface IWicket
{
    int WicketCount { get; set; }
}
public class CricketTeam : ITeam, IWicket
{
    public string? Name { get; set; }
    public int BallCount { get; set; }
    public int GoalCount { get; set; }
    public int WicketCount { get; set; }
}
public class PoolTeam : ITeam
{
    public string? Name { get; set; }
    public int BallCount { get; set; }
    public int GoalCount { get; set; }
}
					

Dependency inversion principle

This principle states:

  • High-level modules should not depend on low-level modules. Both should depend on the abstraction
  • Abstractions should not depend on details. Details should depend on abstractions
Dependency inversion
Image Credit: medium.com

Let's check the following example that applies this principle:

						public interface IRemoteControlService
{
    public void PressOnButton();
}
public interface ITVService
{
    public void TurnOn();
}
public class RemoteControlService : IRemoteControlService 
{
    private readonly ITVService _tvService;
    public RemoteControlService(ITVService tvService)
    {
        _tvService = tvService;
    }
    public void PressOnButton()
    {
        _tvService.TurnOn();
    }
}
					

This example has the loose coupling between the TV and remote control, which allows to replace one of them without necessary affecting the other. In addition, when reading the abstractions in this code we can understand the process that is doing and what the result will be without looking at the details.