Applying SOLID Principles with C#
A practice approach to creating stable software.
![Don’t make your software unstable like a house of cards, solidify it. [via turbosquid]](https://cdn-images-1.medium.com/max/2000/0*ZF6SSZ2bunVOjtXq.jpg)
There are five principles to follow when we write code with object-oriented programming to make it more readable and maintainable if we don’t want that our program becomes a clock bomb in our hands. These principles together compose the acronym SOLID which means respectively.
-
Single responsibility principle.
-
Open-closed principle.
-
Liskov substitution principle.
-
Interface segregation principle.
-
Dependency Inversion Principle.
Each one of these principles has a big importance, and together it will make the architecture of our code as the acronym says, a solid architecture, something that isn’t unstable.
So, below, we will approach each one of these principles briefly to turn the understanding less complex.
Single responsibility principle
![Don’t have classes that do everything like a Swiss knife, separate responsibilities from it. [via euro-knife]](https://cdn-images-1.medium.com/max/2000/0*Xx1gcwfEoObCCogU)
This principle says:
A class should have one and only one reason to change.
It means that a class must only be responsible for one context, that is, one thing, and what isn’t of its context will be out of the class.
Below there is an example where we have more than one responsible for our class.
The Problem.
/// This class represents a person
public class Person
{
public int Id { get; set; }
public string ITIN { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
/// Verify if this data is valid
public bool Validate()
{
return string.IsNullOrWhiteSpace(Name) && BirthDate == DateTime.MinValue && !ValidateITIN(ITIN);
}
/// Validate Individual Taxpayer Identification Number (ITIN)
public static bool ValidateITIN(string document)
{
return new Regex(@"^(9\d{2})([ \-]?)([7]\d|8[0-8])([ \-]?)(\d{4})$").IsMatch(document);
}
/// Save person
public void Save()
{
const string queueName = "Person";
// Get rabbitMQ connection
IConnection connection = GetConnection();
// Create Queue
CreateQueue(queueName, connection);
// Publish
Publish(JsonConvert.SerializeObject(this), queueName, connection);
}
/// Get the RabbitMQ connection
public IConnection GetConnection()
{
return new ConnectionFactory
{
HostName = "localhost",
UserName = "xpto",
Password = "12345"
}.CreateConnection();
}
/// Create the de queue
public QueueDeclareOk CreateQueue(string queueName, IConnection connection)
{
QueueDeclareOk queue;
using (var channel = connection.CreateModel())
{
queue = channel.QueueDeclare(queueName, false, false, false, null);
}
return queue;
}
/// Publish the data
public bool Publish(string message, string queueName, IConnection connection)
{
using (var channel = connection.CreateModel())
{
channel.BasicPublish(string.Empty, queueName, null, Encoding.ASCII.GetBytes(message));
}
return true;
}
}
If we try to count it, we will realize that there are at least 5 responsibilities that shouldn’t be there. So, the person class not must know how to persist itself or how to validate an individual taxpayer identification or how to create and how to use the RabbitMQ as well.
The Solution.
So we need to segregate each responsibility into different classes so if something change we know that we just need to change in that class, and it will not affect the other components, and it will make our code more testable and readable.
The responsibility of the person is to provide valid data.
public class Person
{
public int Id { get; set; }
public string ITIN { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Name) &&
BirthDate != DateTime.MinValue &&
!IdentificationService.ValidateITIN(ITIN);
}
}
The identification service has only the responsibility to tell us if the ITIN is valid.
public static class IdentificationService
{
public static bool ValidateITIN(string document)
{
return new Regex(@"^(9\d{2})([ \-]?)([7]\d|8[0-8])([ \-]?)(\d{4})$").IsMatch(document);
}
}
The EventConnectionFactory provides us with the connection.
public class EventConnectionFactory
{
public IConnection GetConnection()
{
return new ConnectionFactory
{
HostName = "localhost",
UserName = "xpto",
Password = "12345"
}.CreateConnection();
}
}
The EventBusService will just only have the behavior of RabbitMQ.
public class EventBusService
{
public QueueDeclareOk CreateQueue(string queueName, IConnection connection)
{
QueueDeclareOk queue;
using (var channel = connection.CreateModel())
{
queue = channel.QueueDeclare(queueName, false, false, false, null);
}
return queue;
}
public bool Publish(string message, string queueName, IConnection connection)
{
using (var channel = connection.CreateModel())
{
channel.BasicPublish(string.Empty, queueName, null, Encoding.ASCII.GetBytes(message));
}
return true;
}
}
And finally, we should use a business class to create our business logic to persist the person data.
public class PersonBusiness
{
const string queueName = "person";
public bool Save(Person person)
{
if (person.IsValid())
{
EventConnectionFactory connectionFactory = new EventConnectionFactory();
// Get rabbitMQ connection
IConnection connection = connectionFactory.GetConnection();
EventBusService busService = new EventBusService();
// Create Queue
busService.CreateQueue(queueName, connection);
// Publish
busService.Publish(JsonConvert.SerializeObject(person), queueName, connection);
return true;
}
return false;
}
}
Open-closed Principle
![Close the door to modifications and welcome extensions with open arms. [via pixelsquid]](https://cdn-images-1.medium.com/max/2000/0*_UNhUNl9cGBph0tQ.jpg)
This principle says:
Objects or entities should be open for extension but closed for modification.
A summary of it, it means that a class should be easily extendable without modifying the class itself.
The Problem.
Let’s imagine we have a class called Check, and the context of this class is mark a presence.
public class Check
{
public int Id { get; set; }
public CheckTypeEnum Type { get; set; }
public string Justification { get; set; }
public DateTime EntryDate { get; set; }
public DateTime ExitDate { get; set; }
}
And here we have the type of this check if, basically if it’s a checking or a checkout.
public enum CheckTypeEnum
{
[Description("Check-IN")]
IN,
[Description("Check-Out")]
OUT
}
And when we try to do something with this data most of the programmers be based on the enum type, and if tomorrow there is a new type of check? Probably you are thinking… I just need to put a new enum type and write my code based on this kind of check.
public void Save(Check check)
{
if (check.Type == CheckTypeEnum.IN)
{
//DO SOMETHING
}
else if (check.Type == CheckTypeEnum.OUT)
{
//DO SOMETHING ELSE
}
}
It’s a bad practice of this because you will modify the code that was already working, and it can be dangerous, and also you won’t make a good unity test.
How to solve it?
The Solution.
In this case, we will create an abstract class to isolate our business logic using an override for each kind of check.
public abstract class CheckService
{
public abstract void CreateCheck(Check check);
}
And here is the icing on the cake.
As I said above, we can isolate the business logic for each kind of check using inheritance, and we can write a new functionality without changing the existing code, and it will prevent situations like we are changing something in the class and needs to adapt out depending on the kind of class.
public class Checkin : CheckService
{
public override void CreateCheck(Check check)
{
//our business rules for a checkout
}
}
public class Checkout: CheckService
{
public override void CreateCheck(Check check)
{
//our business rules for a checkout
}
}
Liskov substitution principle
![If it looks like a duck, quacks like a duck, but needs batteries. You probably have the wrong abstraction. [via iStock]](https://cdn-images-1.medium.com/max/2000/0*n3p8xQBappHW8jR6)
This principle says:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
If you read it, you can be confused, but translating it into the developers’ world it becomes.
Derived classes must be substitutable for their base classes.
It’s better now, right? 😄
The Problem.
The following code approaches it.
We have a class Apple, this class has a virtual method that can be extendable for the subclasses.
public class Apple
{
public virtual string GetColor()
{
return "Red";
}
}
Here we have a class Orange that extends Apple and overrides the GetColor method.
public class Orange: Apple
{
public override string GetColor() => "Orange";
}
Pay attention to this situation. Theoretically, the Orange is an Apple. It extends that, and I can instantiate like the code below without problem because both are the same thing.
Apple fruit = new Orange();
Console.WriteLine("An apple is " + fruit.GetColor());
Now we will run the code below, and we will get the following result.
An apple is Orange
We must take care when we use inheritance because if the code is not respecting this principle the behavior of the functionality can be the opposite of what we want.
Let’s imagine that we are coding an application to throw a missile, in this case, we will throw it to the wrong place, and it can be expensive and harmful.
The Solution.
In this case, we should have a generic cl