Achieving SOLID principles using C#

To start on with SOLID principles, first of all, what does it stand for…?

Single Responsibility, 

Open-Closed Principle, 

Liskov substitution,

Interface segregation and 

Dependency inversion.


How did it evolve…?

In the early 2000s, Robert Martin defined these object-oriented programming principles to help create a maintainable software system and more adaptive to the changes. SOLID principles are the guidelines to eliminate the code smells by refactoring the source code to achieve the extensible nature, usually required in adaptive programming models.

Single Responsibility Principle

Too many responsibilities on a single entity results into too much of chaos at some point.

One of the best examples is a Swiss Knife. Whenever you want to use one of the cutters from the Swiss Knife like a nail-cutter, you might have to disturb the other cutters to get the one you desired in line for the use. Do we really need to have all the cutters in one single instrument??? Not really, because we can literally have an isolated nail-cutter and similarly, we can have a simple knife, scissor, etc. separately.

Take a look at the code below….which is overloaded with database code and the error logging code.

public class CustomerRepository
{
   public void Update(Customer customer)
   {
      try
      {
         ...
         sqlCommand.ExecuteNonQuery();
         ...
      }
      catch (Exception exception)
      {
         File.WriteAllText(@"C:\logs\logs.txt",exception.ToString());
      }
   }
}

Take a look at the code below….which is overloaded with database code and the error logging code.

public class CustomerRepository
{
   private readonly TextFileLogger _logger;
   
   public CustomerRepository(TextFileLogger logger)
   {
      _logger = logger;
   }
   public void Update(Customer customer)
   {
      try
      {
         ...
         sqlCommand.ExecuteNonQuery();
         ...
      }
      catch (Exception exception)
      {
         _logger.Write("Customer update failed", exception);
      }
   }
}

public class TextFileLogger
{
   private readonly string _logFilePath;

   public TextFileLogger(string logFilePath)
   {
      _logFilePath = logFilePath;
   }

   public void Write(string message, Exception exception = null)
   {
      string log = string.Format("{0} - {1}", message, exception.ToString());

      File.WriteAllText(_logFilePath, log);
   }
}

In the code above, CustomerRepository has a single responsibility of updating the customer details into the database. The logging concern is taken care by TextFileLogger. So this way, the TextFileLogger becomes much more reusable in other components as well.

Open Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

For a class or interface to be Open-Closed, it should be available for extension and, at the same time, be available for use by other modules. Extension can be considered as adding new features to a descendant derived from it. Creating a descendant from the actual class or interface doesn’t impact the existing consumers of the class.

For example, let’s consider there’s a bank which different categories of customers like Silver, Gold and Platinum. And each category of customer has it’s own rate of interest.

Now this is how we can achieve this with Open-Closed principle,

public abstract class Customer
{
   public string Name { get; set; }
   
   public decimal RateOfInterest { get; protected set; }

   protected abstract void SetRateOfInterest();
}

public class PlatinumCustomer : Customer
{
   protected void SetRateOfInterest()
   {
      this.RateOfInterest = 15;
   }
}

public class GoldCustomer : Customer
{
   protected void SetRateOfInterest()
   {
      this.RateOfInterest = 10;
   }
}

public class SilverCustomer : Customer
{
   protected void SetRateOfInterest()
   {
      this.RateOfInterest = 5;
   }
}

Looking at the code above, the Customer class is the one that explains us the Open-Closed principle where it’s not actually closed for changes but open for extensions like the RateOfInterest. Instead of adding selective logic to get specific rate of interests in Customer class, we have just created different descendants for Customer. Each of  the descendant will have it’s own rate of interest which doesn’t impact the existing Customer class and the other descendants as well.

Liskov Substitution Principle

The objects in a program should be replaceable with instances of their sub-types without altering the correctness of that program.

If S is a sub-type of T, then objects of type T may be replaced with objects of type S (i.e., an object of the type T may be substituted with its sub-type object of the type S) without altering any of the desirable properties of that program (correctness, task performed, etc.)

It basically points to a form of type polymorphism in which a sub-type is a datatype that is related to another datatype (the super-type) by some notion of substitutability, meaning that program elements, typically subroutines or functions, written to operate on elements of the super-type can also operate on elements of the sub-type.

Now this is how we can achieve this with Liskov Substitution principle,

public class Quadrilaterals
{
   public virtual int Height { get; set; }
   
   public virtual int Width { get; set; }

   protected int GetArea()
   {
      return Height * Width;
   }
}

public class Rectangle : Quadrilaterals
{
   public override int Width
   {
     get { return base.Width; }
     set { base.Width = value; }
   }
   public override int Height
   {
      get { return base.Height; }
      set { base.Height = value; }
   }
}

public class Square : Quadrilaterals
{
    public override int Width
    {
      get { return base.Width; }
      set { base.Width = value; }
    }

    public override int Height
    {
      get { return base.Height; }
      set { base.Height = value; }
    }
}