If you’re a .NET developer then you might understand the significance of good design.As developers we focus on the implementing the workable solution and often tend to neglect some of the good design practices, as there may not be any implication in the short term.But you might have come across an application only to realize how difficult it is to make trivial changes.
SOLID is an acronym for five design principles.SOLID principles can help make the code more reliable, maintainable, and easy to change. Let’s dive into each principle briefly, with a simple example of how it can be applied to everyday coding scenarios.
Single Responsibility Principle (SRP)
According to this principle each class should do one and only one thing. If you have a class handling user data and logging together, you should split them up ,one for user operations and one for logging.
This means that if want to update logging then you should only change the logging class. This implies change in logging requires change in logging class. So we can say that according to this principle “A class should have only one reason to change.”
public class UserService
{
public void AddUser() { /* code for adding user */ }
}
public class Logger
{
public void Log(string message) { /* code for logging */ }
}
This principle helps in keeping your code focused.
Open/Closed Principle (OCP)
When we are working on any existing application then we often need to enhance it with new features.This may appear to be straightforward approach, "modify the existing implementation". But if we consider the implication of this then we may realize that this can easily break the existing implementation, which might be already operational and tested.
According to the open closed principle you can add new features by adding new code, rather than changing existing code.
“Classes should be open for extension, but closed for modification.”
Assume that you have multiple ways to process payments. Instead of changing one class, you can just add new classes for each payment method.So if use the principle then we add new payment methods without altering core payment logic. This reduces risk when making updates.
public interface IPayment
{
void ProcessPayment();
}
public class CreditCardPayment : IPayment
{
public void ProcessPayment() { /* credit card payment code */ }
}
public class PayPalPayment : IPayment
{
public void ProcessPayment() { /* PayPal payment code */ }
}
Liskov Substitution Principle (LSP)
According to this principle if a class is used as a base, any subclass should work just as well.Lets try to understand this principe with a simple example you might have come across.Let's say you implement repository pattern in your application.
public interface ICustomerRepository
{
Customer GetCustomerById(int id);
}
public class CustomerRepository : ICustomerRepository
{
public Customer GetCustomerById(int id)
{
// Retrieve customer from database
return new Customer { Id = id, Name = "John Doe" };
}
}
Now for testing purpose you need to implement mock MockCustomer repository, which you implement as:
public class BrokenMockCustomerRepository : ICustomerRepository
{
public Customer GetCustomerById(int id)
{
throw new NotImplementedException("Mock not fully implemented");
}
}
If you look carefully you will find that here we can not substitute BrokenMockCustomerRepository class with CustomerRepository class.This is because BrokenMockCustomerRepository is throwing an exception which CustomerRepository is not.But if you consider this principle it says “Derived classes should be substitutable for their base classes.” So we can implement MockCustomerRepository as:
public class CorrectMockCustomerRepository : ICustomerRepository
{
public Customer GetCustomerById(int id)
{
return new Customer { Id = id, Name = $"Mock Customer {id}" };
}
}
This will ensure that bot MockCustomerRepository and CustomerRepository behave consistently ,avoiding any unexpected behavior.
Interface Segregation Principle (ISP)
If you have worked on any application development then chances are that you might have across interfaces which implement may methods which might be unrelated.Though it might not appear to be a significant problem.But let's try to understand the problem this creates with a simple example.Let's say you have a printer class which is used to print documents using a printer. Now there are many end users of your class which don't have scan on there printer.
So if you consider this principle it says “Don’t force a class to implement methods it doesn’t use.”
So if you have implemented your interface as below , then it's violating this principle as the IPrinter interface is implementing both the methods.Printer class shouldn’t have to implement Scan if it only prints.
public interface IPrinter
{
void Print();
void Scan()
}
So we can rewrite the IPrinter interface as:
public interface IPrinter
{
void Print();
}
We can separate scan method in a different interface:
public interface IScanner
{
void Scan();
}
public class OfficePrinter : IPrinter, IScanner
{
public void Print() { /* print code */ }
public void Scan() { /* scan code */ }
}
This helps in keeping your interfaces focused and avoid forcing classes to handle methods that aren’t relevant.
Dependency Inversion Principle (DIP)
This principle states “Depend on abstractions, not on concrete implementations.”
Following this principle means your classes should rely on interfaces or abstract classes instead of specific implementations. If an OrderProcessor class directly uses a CreditCardPayment class, it’s limited. Instead, use an IPayment interface.
public interface IPayment
{
void ProcessPayment();
}
public class OrderProcessor
{
private readonly IPayment _payment;
public OrderProcessor(IPayment payment) { _payment = payment; }
public void ProcessOrder() { _payment.ProcessPayment(); }
}
This makes your code flexible by depending on interfaces. You can swap out implementations more easily.
Top comments (0)