SOLID Principles In C#

Charles
7 min readJan 27, 2024

--

Introduction to the SOLID Principles

Solid principles are sets of 5 principles and guidelines to be followed by developers that need to create good and maintainable software.

Knowing these principles is a good head start that guides you to software architectures like Domain-driven design.

Applying the solid principles to software helps you avoid things like untestable code, repeatable code, tightly coupled code, and code that can’t easily evolve.

SOLID

SOLID is a mnemonic acronym that stands for 5 principles in software development. These principles are:

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

Let’s discuss about these principles step by step and apply it in code and, starting with the SRP principle.

Single Responsibility Principle

The single responsibility principle states that a software module (class) should only have one reason to change. If a class is doing many things, it is not responsible for one thing and becomes tightly coupled and hard to maintain and hard to test.

For example, if you have a class that manages a customer and also has logging code contained in it, this class violates the SRP principle. You should rather have the logging functionality in a single class and the class should just be responsible for logging purposes and if you need to change the logging code, you can just modify only that class.

Writing code before applying SRP is also a better approach because you can just write your code and in the long run when you start experiencing PDD(Pain Driven Development) or a class starts getting complicated, you can apply the SRP principle by breaking down this class.

SRP Example

The Customer class is responsible for managing customers. The code only provide method to add customer and to display customer. If we all code to log to a file or console in the Customer class, it breaks the rules of the SRP principles.

using System.Text.Json;

public class Customer
{
List<CustomerDTO> lst = null;
public Customer()
{
lst = new();
}
public void AddCustomer(CustomerDTO customer)
{
lst.Add(customer);
Log.AddLogs($"{customer.Fullname} added");
}

public string DisplayCustomer()
{
string customer = JsonSerializer.Serialize(lst);
Log.AddLogs($"customers retrieved\n{customer}");
return customer;
}

//lets take this to another class
/*void AddLogs(string message)
{
Console.WriteLine(message)
}
*/

}
public static class Log
{
public static void AddLogs(string message)
{
Console.WriteLine(message);
}
}

Now the Log class is responsible for doing one thing and if need a reason to change this class, it can done in one place.

Open/Closed Principle

The OCP states that a class should be open for extension but closed for modification. It is similar to the strategy design pattern.

The idea of OCP is to avoid breaking existing clients. If a class is already built and used in production, you shouldn’t modify the code, instead, you should extend the class and call the methods you need.

OCP Example

We want to allow users of our application to be able to create different kind of profiles. The use can decide to create a basic profile and a job profile later. To do this without following the OCP principle, we would have to write the code in a class and use conditional statements to check the ProfileType.

Non-OCP Code

using System.Text.Json;

public class Profile
{
List<object> lst = new();
public void AddProfile(object profileInfo, ProfileType type)
{
if(type == ProfileType.Basic){
lst.Add(profileInfo);
}
//if we want to add another profile type, this code has to be modified and it breaks the OCP principles
}
public string ViewProfile()
{
string result = JsonSerializer.Serialize(lst);
return result;
}
}

If we want to add another type of profile, this piece of code has to be modified therefore breaking the OCP principles.

OCP Code

We can have the profile types defined in their own class and each of class should implement an Interface. The we can now wrap the class using a with another class that knows about the contract interface that the different profile types implements.

using System.Text.Json;

public class JobProfile : IProfile
{
List<object> lst = new();
public void AddProfile(object profileInfo)
{
lst.Add(profileInfo);
}

public string ViewProfile()
{
string result = JsonSerializer.Serialize(lst);
Console.WriteLine(result);
return result;
}
}
using System.Text.Json;

public class BasicProfile : IProfile
{
List<object> lst = new();
public void AddProfile(object profileInfo)
{
lst.Add(profileInfo);
}

public string ViewProfile()
{
string result = JsonSerializer.Serialize(lst);
Console.WriteLine(result);
return result;
}
}
using System.Text.Json;

public class Profile
{
IProfile _profile;
public Profile(IProfile profile) //has a reference to IProfile
{
_profile = profile;
}
public void AddProfile(object profileInfo) //add new functionality without changing existing one
{
_profile.AddProfile(profileInfo);
}
public string ViewProfile()
{
return _profile.ViewProfile();
}
}

We can call it like this:


Profile profile = new Profile(new JobProfile());
Tuple<string,int> basic = new Tuple<string, int>("John",20);
profile.AddProfile(basic);
profile.ViewProfile();

Liskov Substitution Principle

The Liskov substitution principle states that subtypes must be substitutable for the base types without breaking the client code.

Non LSP Code

using System.Runtime.Versioning;

public class Vehicle
{
public abstract string Color();
}
public class Lexus : IVehicle
{
public override string Color()=>"black";
}

public class Mercedes : Lexus
{
public override string Color()=>"white";
}

//possible because of the inheritance. 
//But in the real sense a lexus is not a mercedes
Lexus lex = new Mercedes();
Console.WriteLine(lex.Color());
//we may be expecting to get color black but we have white.
//this breaks the LSP principles

We may be expecting to get color black but we have white and this breaks the LSP principle because the subtype is not substitutable.

LSP Code

using System.Runtime.Versioning;

public interface IVehicle
{
public string Color();
}
public class Lexus : IVehicle
{
public string Color()=>"black";
}

public class Mercedes : IVehicle //can also inherit Lexus
{
public string Color()=>"white";
}

/*
public class Mercedes : Lexus
{
public string Color()=>"green";
}
*/
IVehicle vehicle = new Lexus(); //subsitutable because the Lexus implement IVehicle
vehicle = new Mercedes();
Console.WriteLine(vehicle.Color());

//if(vehicle is IVehicle) works
//Lexus lex = new Mercedes();
//Console.WriteLine(lex.Color());// now we have the desired result

Interface Segregation Principle

The Interface Segregation Principle states that clients shouldn’t be forced to depend on methods they do not use.

For example, if you have a fat interface that contains several methods and you have a class that doesn’t need some of these methods when you implement them, you can break down the interface into smaller interfaces and implement the smaller interface. But if another class needs all methods, that class can implement all interfaces that were broken down. In this way, clients are not forced to depend on methods they don’t use.

ISP Example

public interface IAdvancedMath
{
public bool IsEven(params int[] nums);
public bool IsOdd(params int[] nums);
}
public interface IBasicMath
{
public int Max(params int[] nums);

public int Min(params int[] nums);

public int Sum(params int[] nums);
}
public class BasicMath : IBasicMath
{
public int Max(params int[] nums)
{
return nums.Max();
}

public int Min(params int[] nums)
{
return nums.Min();
}

public int Sum(params int[] nums)
{
return nums.Sum();
}
}

The basic math class needs on basic math functionality, so we implement the IBasicMath class. But the AdvancedMath class needs both basic and advanced math functionalities so we implement both interfaces. therefore, we are not forced to depend on methods we don’t need.

public class AdvancedMath : IMath, IBasicMath
{
public bool IsEven(params int[] nums)
{
bool isEven = nums.Sum()%2==0;
return isEven;
}

public bool IsOdd(params int[] nums)
{
bool isOdd = nums.Sum()%2!=0;
return isOdd;
}

public int Max(params int[] nums)
{
return nums.Max();
}

public int Min(params int[] nums)
{
return nums.Min();
}

public int Sum(params int[] nums)
{
return nums.Sum();
}
}

Dependency Inversion Principle

The Dependency Inversion Principle states that High-level classes should not depend on Low-level classes. Both should depend on abstractions.

Abstractions are interfaces or abstract classes in c#.

For example, I have a high-level class that contains a complex business class. This class can be a Discount class that calculates the discount on a cart item. This class should depend on abstraction, it should not depend on low-level classes like the SamsungDiscount class. Let the high-level class take a reference to the abstraction (interface) through its constructor, and the low-level class should implement the interface. You can wrap the low-level class into the high-level class.

DIP Example

public interface ILevel
{
public string GetMessage();
}
public class HighLevel
{
private readonly ILevel level;
public HighLevel(ILevel level) //depending on abractions
{
this.level = level;
}
public string DisplayMessage()
{
return level.GetMessage();
}
}
public class LowLevel : ILevel
{
public string GetMessage()
{
return $"In {nameof(LowLevel)}";
}
}
HighLevel highLevel = new HighLevel(new LowLevel());
Console.WriteLine(highLevel.DisplayMessage());

In ASP.NET Core, there is support for DI automatically and if you want to inject this class in the controller through its constructor, you can just pass the ILevel interface and assign it in the constructor. In the program.cs class you would inject it by doing this:

builder.Services.AddScoped<ILevel, LowLevel>();

Conclusion

So far, we have learned the SOLID principles which are guidelines developers should follow to build robust and maintainable softwares.

To get a deeper understanding of SOLID principles, enrol for this course on Domain-Driven Design in DotNet Core that provides in-depth knowledge of Domain-Driven Design and SOLID principles for becoming a Next Level Software Engineer.

Cheers!

--

--