Solid Principles in CSharp
Here I would like to explain Solid Principles in CSharp
Acronym for SOLID Principles:
S-Single responsibility principle
O-Open closed Principle
L-Liskov substitution principle
I-Interface segregation principle
D-Dependency princi9ple
Here I would like to explain Solid Principles in CSharp
Acronym for SOLID Principles:
S-Single responsibility principle
O-Open closed Principle
L-Liskov substitution principle
I-Interface segregation principle
D-Dependency princi9ple
Single Responsibility principle
This principal indicates that a entity or a class should have one and only one responsibility
Example
Customer class should contain information about real life customer. Add the properties and methods related to customer like
Properties: customername,CustomerID..etc
Methods : InsertCustomer,UpdateCustomer,DeleteCustomer,Validate
public class Customer
{
public string customerName { get; set; }
public string customerID { get; set; }
public void Insert()
{
//insert logic
}
public void Update()
{
//insert logic
}
public void Delete()
{
//Delete logic
}
public void Validate()
{
try
{
//Validate logic
}
catch (Exception ex)
{
System.IO.File.WriteAllText("Log.txt", ex.ToString());
}
}
}
The above customer class is also doing log activity in validate method which is not supposed to do. Customer should only do customer activities.
If any error occurs in the log we need to modify the customer class which not correct.
Open Closed Principle
The open closed principle says A class is open for extension and closed for modification. A class is having certain functionality. A few months later some new requirements arises and class is supposed to provide some functionality. In such cases we should not modify the code which is working instead should be able to extend the class.
public class Salary
{
public decimal calculatesalary(string emp)
{
decimal sal = 0;
if (emp == "Software engineer")
{
sal = 500;
}
else if (emp == "Analyst")
{
sal = 10000;
}
return sal;
}
}
The above class is calculating the salary of the different employees. After few months we also need to calculate the salary of some other employee types. We have to add one more if block. That means any change in the functionality is forcing you to change the core class. to avoid this we have to extend the class as below.
public abstract class Salary
{
public abstract decimal calculatesalary(string emp);
}
public class SoftwareEngineer:Salary
{
public override decimal calculatesalary(string emp)
{
return 500;
}
}
public class AnalysteEngineer : Salary
{
public override decimal calculatesalary(string emp)
{
return 10000;
}
}
The above code creates an abstract class defines abstract method calculatesalary.Tomorrow if salary calculate needs to be calculated for other employees there is no need to modify the built code simple we can add new class which inherits the abstract class.
Liskov Substitution Principle
This Principle states that Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Example
Let's create class Special Customers that maintains a list of special Customers
public class SpecialCustomers
{
List
public virtual void AddCustomer(Customer obj)
{
list.Add(obj);
}
public int Count
{
get
{
return list.Count;
}
}
The AddCustomer() method accepts an instance of Customer and adds to the generic List. The Count property returns the number of Customer elements in the List.
There is another class TopNCustomers - that inherits from the SpecialCustomers class
public class TopNCustomers:SpecialCustomers
{
private int maxCount = 5;
public override void AddCustomer(Customer obj)
{
if (Count < maxCount)
{
AddCustomer(obj);
}
else
{
throw new Exception("Only " + maxCount + " customers can be added.");
}
}
}
TopNCustomers class overrides the AddCustomer() method of the SpecialCustomers base class. The new implementation checks whether the customer count is less than maxCount (5 in this case). If so, the Customer is added to the List else an exception is thrown.
SpecialCustomers sc = null;
sc = new TopNCustomers();
for (int i = 0; i < 10; i++)
{
Customer obj = new Customer();
sc.AddCustomer(obj);
}
The code declares a variable of type SpecialCustomers and then points it to an instance of TopNCustomers. This assignment is perfectly valid since TopNCustomers is derived from SpecialCustomers. The problem comes in the for loop. The for loop that follows attempts to add 10 Customer instances to the TopNCustomers. But TopNCustomers allows only 5 instances and hence throws an error. If sc would have been of type SpecialCustomers the for loop would have successfully added 10 instances into the List. However, since the code substitutes TopNCustomers instance in place of SpecialCustomers the code produces an exception. Thus LSP is violated in this example.
public abstract class CustomerCollection
{
public abstract void AddCustomer(Customer obj);
public abstract int Count { get; }
}
public class SpecialCustomers:CustomerCollection
{
List
public override void AddCustomer(Customer obj)
{
list.Add(obj);
}
public override int Count
{
get
{
return list.Count;
}
}
}
public class TopNCustomers : CustomerCollection
{
private int count=0;
Customer[] list = new Customer[5];
public override void AddCustomer(Customer obj)
{
if(count<5)
{
list[count] = obj;
count++;
}
else
{
throw new Exception("Only " + count + " customers can be added.");
}
}
public override int Count
{
get
{
return list.Length;
}
}
}
}
new set of classes can then be used as follows:
Customer c = new Customer() { CustomerID = "ALFKI" };
CustomerCollection collection = null;
collection = new SpecialCustomers();
collection.AddCustomer(c);
collection = new TopNCustomers();
collection.AddCustomer(c);
Interface Segregation Principle
No client should be forced to depend on methods it does not use.
Many client-specific interfaces are better than one general-purpose interface.
We have a customer class which is used by several clients new clients come up with a demand saying that we also want a method which will help us to "Read" customer data. So developers change the interface as below
interface IDatabase
{
void Add();
voidRead();
}
few clients wants to aces add method few of them access Read method.
By changing the current interface we are disturbing the old client's use the "Read" method. So a better approach would be to keep existing clients not to disturb and serve the new clients separately better solution would be to create a new interface rather than updating the current interface.
interface IDatabaseV1 : IDatabase
{
Void Read();
}
class CustomerwithRead : IDatabase, IDatabaseV1
{
public void Add()
{
Customer obj = new Customer();
Obj.Add();
}
Public void Read()
{
} }
Dependency Inversion Principle (DIP)
This principal state 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.
If we need to work on an error logging module that logs exception stack traces into a file. Below is 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 string GetUserReadableMessage(Exception ex)
{
string strMessage = string. Empty;
return strMessage;
}
}
client class exports data from many files to a database.
public class DataExporter
{
public void ExportDataFromFile()
{
try {
//code to export data from files to database.
}
catch(Exception ex)
{
new ExceptionLogger().LogIntoFile(ex);
}
}
}
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.
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);
}
}
}
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 classes. 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
public interface ILogger
{
public 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);
}
}
}
We successfully removed the dependency on low-level classes. This ExceptionLogger doesn't depend on the FileLogger and EventLogger classes to log the stack trace. We don't need to change the ExceptionLogger's code any more for any new logging functionality. We need to create a new logging class that implements the ILogger interface and must add another catch block to the DataExporter class's ExportDataFromFile method.
public class EventLogger: ILogger
{
public void LogMessage(string aMessage)
{
//Code to write message in system's event viewer.
}
}
And we need to add a condition in the DataExporter class as in the following:
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(SqlException ex)
{
_exceptionLogger = new ExceptionLogger(new EventLogger());
_exceptionLogger.LogException(ex);
}
catch(Exception ex)
{
_exceptionLogger = new ExceptionLogger(new FileLogger());
_exceptionLogger.LogException(ex);
}
}
}