SOLID architecture principle using C# with simple C# example
Introduction
In this blog, I am going to
explain you the SOLID architecture principle using simple C# code. This would
help to build application with layer architecture with readable and easily
maintainable code.
What is SOLID?
SOLID principles are the design
principles that enable us to manage with most of the software design problems.
These principles provide us ways to move from tightly coupled code and little
encapsulation to the desired results of loosely coupled and encapsulated real
needs of a business properly.
Below are the acronym of an SOLID.
S: Single Responsibility
Principle (SRP).
O: Open closed Principle
(OSP).
L: Liskov substitution
Principle (LSP).
I: Interface Segregation
Principle (ISP).
D: Dependency Inversion
Principle (DIP).
Let’s go walk through each of
them below.
S: Single Responsibility Principle (SRP)
“There should never be more than one reason for
a class to change”
Simple Translation: A class should concentrate
on doing one thing The SRP says a class should focus on doing one thing,
or have one responsibility. This doesn’t mean it should only have one method,
but instead all the methods should relate to a single purpose (i.e. should be
cohesive).
For example, an Invoice class might have the responsibility
of calculating various amounts based on its data. In that case it probably
shouldn’t know about how to retrieve this data from a database, or how to
format an invoice for print or display or logging, sending Email etc.
A class that adheres to the SRP should be easier to change than those with
multiple responsibilities. If we have calculation logic and database logic and
display logic all mixed up within one class it can be difficult to change one
part without breaking others.
Mixing responsibilities also makes the class harder to understand, harder to
test, and increases the risk of duplicating logic in other parts of the design
Violations of the SRP
public class Invoice
{
public long Amount { get; set; }
public DateTime InvoiceDate { get; set; }
public void Add()
{
try
{
// Code for adding invoice
// Once Invoice has been added ,
send mail
MailMessage mailMessage = new
MailMessage("MailAddressFrom","MailAddressTo","MailSubject","MailBody");
this.SendEmail(mailMessage);
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
}
}
public void Delete()
{
try
{
// Code for Delete invoice
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
}
}
public void SendEmail(MailMessage
mailMessage)
{
try
{
// Code for getting Email setting
and send invoice mail
}
catch (Exception ex)
{
System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
}
}
}
This Invoice class violating SRP,
as It has his own responsibility i.e. Add, Delete invoice and also has extra
activity like logging and Sending email as well.
Solution, lets refactor it.
public class Invoice
{
public long Amount { get; set; }
public DateTime InvoiceDate { get; set; }
private FileLogger fileLogger;
private MailSender mailSender;
public Invoice()
{
fileLogger = new FileLogger();
mailSender = new MailSender();
}
public void Add()
{
try
{
fileLogger.Info("Add method
Start");
// Code for adding invoice
// Once Invoice has been added ,
send mail
mailSender.From =
"rakesh.girase@thedigitalgroup.net";
mailSender.To = "customers@digitalgroup.com";
mailSender.Subject =
"TestMail";
mailSender.Body = "This is a
text mail";
mailSender.SendEmail();
}
catch (Exception ex)
{
fileLogger.Error("Error while
Adding Invoice", ex);
}
}
public void Delete()
{
try
{
fileLogger.Info("Add Delete
Start");
// Code for Delete invoice
}
catch (Exception ex)
{
fileLogger.Error("Error while
Deleting Invoice", ex);
}
}
}
public interface ILogger
{
void Info(string info);
void Debug(string info);
void Error(string message, Exception ex);
}
public class FileLogger
: ILogger
{
public FileLogger()
{
// Code for initialization i.e.
Creating Log file with specified
// details
}
public void Info(string info)
{
// Code for writing details into text
file
}
public void Debug(string info)
{
// Code for writing debug information
into text file
}
public void Error(string message, Exception
ex)
{
// Code for writing Error with message
and exception detail
}
}
public class MailSender
{
public string From { get; set; }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public void SendEmail()
{
// Code for sending mail
}
}
Now Invoice class can happily
delegate the logging activity to the “FileLogger” class and Sending mail
activity to “MailSender” class. This way Invoice class can concentrate on
Invoice related activities.
O:
Open closed Principle (OSP)
“Software entities (classes, modules, functions, etc.) should be
open for extension, but closed for modification.”
Simple Translation: Change
a class’ behavior using inheritance and composition.
Here “Open for extension” means,
we need to design our module/class in such a way that the new functionality can
be added only when new requirements are generated. “Closed for modification”
means we have already developed a class and it has gone through unit testing.
We should then not alter it until we find bugs. As it says, a class should be
open for extensions, we can use inheritance to do this.
Let’s continue with our same
Invoice class example. I have added a simple Invoice type property to the
class. This property decided if this is a “Final” Or “Proposed” invoice.
Depending on the same it
calculates discount. Have a look at the “GetDiscount” function which returns
discount accordingly.
Violation of OSP
public enum InvoiceType
{
Final,Proposed
};
public class Invoice
{
public InvoiceType InvoiceType { get; set;
}
public double GetDiscount(double
amount,InvoiceType invoiceType)
{
double finalAmount = 0;
if (invoiceType == InvoiceType.Final)
{
finalAmount = amount - 100;
}
else if(invoiceType == InvoiceType.Proposed)
{
finalAmount = amount - 50;
}
return finalAmount;
}
}
The problem is if we add a new
invoice type, we need to go and add one more “IF” condition in the
“GetDiscount” function, in other words we need to change the invoice class.
If we are changing the Invoice
class again and again, we need to ensure that the previous conditions with new
one’s are tested again , existing client’s which are referencing this class are
working properly as before.
In other words we are
“MODIFYING” the current invoice code for every change and every time we modify
we need to ensure that all the previous functionality and connected client are
working as before.
How about rather than
“MODIFYING” , we go for “EXTENSION”. In other words every time a new invoice
type needs to be added we create a new class as shown in the below. So whatever
is the current code they are untouched and we just need to test and check the
new classes.
Solution, let’s refactor it
namespace
SOLID_Principles_OSP_S
{
public enum InvoiceType
{
Final, Proposed
};
public class Invoice
{
public InvoiceType InvoiceType { get;
set; }
public virtual double
GetDiscount(double amount)
{
double finalAmount = 300;
return finalAmount;
}
}
public class FinalInvoice : Invoice
{
public override double
GetDiscount(double amount)
{
return base.GetDiscount(amount) -
100;
}
}
public class ProposedInvoice : Invoice
{
public override double
GetDiscount(double amount)
{
return base.GetDiscount(amount) -
50;
}
}
public class RecurringInvoice : Invoice
{
public override double
GetDiscount(double amount)
{
return base.GetDiscount(amount) -
200;
}
}
}
Putting in simple words the
“Invoice” class is now closed for any new modification but it’s open for
extensions when new Invoice types are added to the project.
L:
Liskov substitution Principle (LSP)
“Objects in a program should be replaceable with instances of
their sub types without altering the correctness of that program”
Simple Translation: We must make sure that new derived classes are extending
the base classes without changing their behavior
LSP states that the derived
classes should be perfectly substitutable for their base classes. If class D is
derived from A then D should be substitutable for A.
Look at the following C# code
sample where the LSP is broken. Simply, an Orange cannot substitute an Apple,
which results in printing the color of apple as Orange.
Violation
of LSP
namespace
SOLID_Principles_LSP_V
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Orange();
Console.WriteLine(apple.GetColor());
}
}
public class Apple
{
public virtual string GetColor()
{
return "Red";
}
}
public class Orange : Apple
{
public override string GetColor()
{
return "Orange";
}
}
}
Solution,
refactor
Now let us re-factor and make it
comply with LSP by having a generic base class for both Apple and Orange.
namespace
SOLID_Principles_LSP_S
{
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Console.WriteLine(fruit.GetColor());
fruit = new Apple();
Console.WriteLine(fruit.GetColor());
}
}
public abstract class Fruit
{
public abstract string GetColor();
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red";
}
}
public class Orange : Apple
{
public override string GetColor()
{
return "Orange";
}
}
}
I:
Interface Segregation Principle (ISP)
“Clients should not be forced to implement interfaces they don’t
use. Instead of one fat interface many small interfaces are preferred based on
groups of methods, each one serving one sub module.“
Simple Translation: ”No client consuming an interface should be forced to depend
on methods it does not use”
Let’s start with an example that
breaks ISP. Suppose we need to build a system for an IT firm that contains
roles like TeamLead and Programmer where TeamLead divides a huge task into
smaller tasks and assigns them to his/her programmers or can directly work on
them.
Based on specifications, we need
to create an interface and a TeamLead class to implement it.
namespace
SOLID_Principles_ISP_V
{
public interface ILead
{
void CreateSubTask();
void AssginTask();
void WorkOnTask();
}
public class TeamLead : ILead
{
public void AssignTask()
{
//Code to assign a task.
}
public void CreateSubTask()
{
//Code to create a sub task
}
public void WorkOnTask()
{
//Code to implement perform
assigned task.
}
}
}
The design looks fine for now.
Later another role like Manager, who assigns tasks to TeamLead and will not
work on the tasks, is introduced into the system. Can we directly implement an ILead interface in the Manager class, like
the following?
public class Manager :
ILead
{
public void AssignTask()
{
//Code to assign a task.
}
public void CreateSubTask()
{
//Code to create a sub task.
}
public void WorkOnTask()
{
throw new Exception("Manager
can't work on Task");
}
}
Since the Manager can’t work on
a task and at the same time no one can assign tasks to the Manager, this
WorkOnTask() should not be in the Manager class. But we are implementing this
class from the ILead interface, we need to provide a concrete Method. Here we
are forcing the Manager class to implement a WorkOnTask() method without a
purpose. This is wrong. The design violates ISP. Let’s correct the design.
Since we have three roles, 1.
Manager, that can only divide and assign the tasks, 2. TeamLead that can divide
and assign the tasks and can work on them as well, 3. Programmer that can only
work on tasks, we need to divide the responsibilities by segregating the ILead
interface. An interface that provides a contract for WorkOnTask().
Solution, lets refactor it
namespace
SOLID_Principles_ISP_S
{
public interface IProgrammer
{
void WorkOnTask();
}
public interface ILead
{
void AssignTask();
void CreateSubTask();
}
public class Programmer : IProgrammer
{
public void WorkOnTask()
{
//code to implement to work on the
Task.
}
}
public class Manager : ILead
{
public void AssignTask()
{
//Code to assign a Task
}
public void CreateSubTask()
{
//Code to create a sub taks from a
task.
}
}
public class TeamLead : IProgrammer, ILead
{
public void AssignTask()
{
//Code to assign a Task
}
public void CreateSubTask()
{
//Code to create a sub task from a
task.
}
public void WorkOnTask()
{
//code to implement to work on the
Task.
}
}
}
Here we separated
responsibilities/purposes and distributed them on multiple Interfaces and
provided a good level of abstraction too.
D:
Dependency Inversion Principle (DIP)
“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.”
Simple Translation: High level module and Low level module keep as loosely
couple as much as we can.
When a class knows explicitly
about the design and implementation of another class, it raises the risk that
changes to one class will break the other class. So we must keep these
high-level and low-level modules/class loosely coupled as much as we can. To do
that, we need to make both of them dependent on abstractions instead of knowing
each other. Let’s start with an example.
Suppose we need to work on an
error logging module that logs exception stack traces into a file. Simple,
isn’t it? The following are the classes that provide functionality to log a
stack trace into a file.
public class FileLogger
{
public void LogMessage(string aStackTrace)
{
//code to log stack trace into a
file.
}
}
public static class
ExceptionLogger
{
public static void LogIntoFile(Exception
aException)
{
FileLogger objFileLogger = new
FileLogger();
objFileLogger.LogMessage(GetUserReadableMessage(aException));
}
private static string
GetUserReadableMessage(Exception ex)
{
string strMessage = string.Empty;
//code to convert Exception's stack
trace and message to user readable format.
return strMessage;
}
}
public class
DataExporter
{
public void ExportDataFromFile()
{
try
{
//code to export data from files to
database.
}
catch (Exception ex)
{
new
ExceptionLogger.LogIntoFile(ex);
}
}
}
Looks good. We sent our
application to the client. But our client wants to store this stack trace in a
database if an IO exception occurs. Hmm… okay, no problem. We can implement
that too. Here we need to add one more class that provides the functionality to
log the stack trace into the database and an extra method in ExceptionLogger to
interact with our new class to log the stack trace.
namespace
SOLID_Principles
{
public class DbLogger
{
public void LogMessage(string aMessage)
{
//Code to write message in
database.
}
}
public class FileLogger
{
public void LogMessage(string
aStackTrace)
{
//code to log stack trace into a
file.
}
}
public class ExceptionLogger
{
public void LogIntoFile(Exception
aException)
{
FileLogger objFileLogger = new
FileLogger();
objFileLogger.LogMessage(GetUserReadableMessage(aException));
}
public void LogIntoDataBase(Exception
aException)
{
DbLogger objDbLogger = new
DbLogger();
objDbLogger.LogMessage(GetUserReadableMessage(aException));
}
private string
GetUserReadableMessage(Exception ex)
{
string strMessage = string.Empty;
//code to convert Exception's stack
trace and message to user
readable format.
return strMessage;
}
}
public class DataExporter
{
public void ExportDataFromFile()
{
try
{
//code to export data from
files to database.
}
catch (IOException ex)
{
new
ExceptionLogger().LogIntoDataBase(ex);
}
catch (Exception ex)
{
new
ExceptionLogger().LogIntoFile(ex);
}
}
}
}
Looks fine for now. But whenever
the client wants to introduce a new logger, we need to alter ExceptionLogger by
adding a new method. If we continue doing this after some time then we will see
a fat ExceptionLogger class with a large set of methods that provide the
functionality to log a message into various targets. Why does this issue occur?
Because ExceptionLogger directly contacts the low-level classes FileLogger and
and DbLogger to log the exception. We need to alter the design so that this
ExceptionLogger class can be loosely coupled with those class. To do that we
need to introduce an abstraction between them, so that ExcetpionLogger can
contact the abstraction to log the exception instead of depending on the
low-level classes directly.
Solution: Lets refactor it
Now, we move to the low-level
class’s intitiation from the ExcetpionLogger class to the DataExporter class to
make ExceptionLogger loosely coupled with the low-level classes FileLogger and
EventLogger. And by doing that we are giving provision to DataExporter class to
decide what kind of Logger should be called based on the exception that occurs.
namespace
SOLID_Principles_DIP_S
{
public interface ILogger
{
void LogMessage(string aString);
}
public class DbLogger : ILogger
{
public void LogMessage(string aMessage)
{
//Code to write message in
database.
}
}
public class FileLogger : ILogger
{
public void LogMessage(string
aStackTrace)
{
//code to log stack trace into a
file.
}
}
public class ExceptionLogger
{
private ILogger _logger;
public ExceptionLogger(ILogger aLogger)
{
this._logger = aLogger;
}
public void LogException(Exception
aException)
{
string strMessage =
GetUserReadableMessage(aException);
this._logger.LogMessage(strMessage);
}
private string
GetUserReadableMessage(Exception aException)
{
string strMessage = string.Empty;
//code to convert Exception's stack
trace and message to user readable format.
return strMessage;
}
}
public class DataExporter
{
public void ExportDataFromFile()
{
ExceptionLogger _exceptionLogger;
try
{
//code to export data from
files to database.
}
catch (IOException ex)
{
_exceptionLogger = new
ExceptionLogger(new DbLogger());
_exceptionLogger.LogException(ex);
}
catch (Exception ex)
{
_exceptionLogger = new
ExceptionLogger(new FileLogger());
_exceptionLogger.LogException(ex);
}
}
}
}
Now the high level (FileLogger
and DBLogger) and low level (DataExporter) models are loosely couple.
Conclusion
We have gone through all the
five SOLID principles successfully with simple C# example.