SOLID Principles Cheat Sheet
If you're starting your journey in software development, understanding the SOLID principles can be a game changer for writing clean, maintainable code. In this article, we'll explore the five SOLID principles with C# examples, including wrong and corrected versions, to help you easily grasp how to apply them in real projects.
What are SOLID Principles?
The SOLID principles are five key design principles that guide developers in creating flexible and maintainable software. These principles make code easier to understand, test, and scale.
SOLID its abbreviation, means:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Let's look into each one separately:
Single Responsibility Principle (SRP)
A class should have only one reason to change. In other words, a class should only have one responsibility.
❌ Single Responsibility Principle Violation
public class OrderManager
{
public void ProcessOrder(Order order)
{
// Logic to process the order
}
public void SaveOrderToDatabase(Order order)
{
// Logic to save order to the database
}
public void SendOrderConfirmationEmail(Order order)
{
// Logic to send confirmation email
}
}
What's wrong?
The OrderManager
the class has multiple responsibilities: processing an order, saving it to a database, and sending a confirmation email. This makes it harder to maintain, as changes to any responsibility could affect others.
✔️ Single Responsibility Principle Compliant
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
// Logic to process the order
}
}
public class OrderRepository
{
public void SaveOrder(Order order)
{
// Logic to save order to the database
}
}
public class EmailService
{
public void SendOrderConfirmationEmail(Order order)
{
// Logic to send confirmation email
}
}
Improvements:
Each class currently follows a single responsibility: OrderProcessor
processes orders, OrderRepository
saves orders, and EmailService
sends emails. This makes the code easier to modify and maintain.
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. This means you should be able to add new functionality without altering existing code.
❌ Open/Closed Principle Violation
public class DiscountCalculator
{
public double CalculateDiscount(Order order, string customerType)
{
if (customerType == "Regular")
{
return order.Amount * 0.05;
}
else if (customerType == "Premium")
{
return order.Amount * 0.10;
}
return 0;
}
}
What's wrong?
Adding a new customer type requires modifying the CalculateDiscount
method, which can lead to potential errors and make the code less flexible.
✔️ Open/Closed Principle Compliant
public interface IDiscountStrategy
{
double CalculateDiscount(Order order);
}
public class RegularCustomerDiscount : IDiscountStrategy
{
public double CalculateDiscount(Order order)
{
return order.Amount * 0.05;
}
}
public class PremiumCustomerDiscount : IDiscountStrategy
{
public double CalculateDiscount(Order order)
{
return order.Amount * 0.10;
}
}
public class DiscountCalculator
{
public double CalculateDiscount(Order order, IDiscountStrategy discountStrategy)
{
return discountStrategy.CalculateDiscount(order);
}
}
Improvements:
The DiscountCalculator
class is now closed for modification but open for extension. You can add new discount strategies by implementing IDiscountStrategy
without changing existing code.
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
❌ Liskov Substitution Principle Violation
public class Bird
{
public virtual void Fly() { }
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotImplementedException();
}
}
What's Wrong?
The Penguin
class cannot fly, but it inherits from the Bird
, which suggests that all birds can fly. Violating LSP leads to unexpected behavior.
✔️ Liskov Substitution Principle Compliant
public class Bird { }
public class FlyingBird : Bird
{
public virtual void Fly() { }
}
public class Sparrow : FlyingBird
{
public override void Fly() { }
}
public class Penguin : Bird
{
// Penguins don't fly, no Fly method needed
}
Improvements:
Penguin
does not inherit the Fly
method, avoiding the Liskov violation. Only birds that can fly inherit from FlyingBird
.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
❌ Interface Segregation Principle Violation
public interface IWorker
{
void Work();
void Eat();
}
public class RobotWorker : IWorker
{
public void Work() { }
public void Eat() { throw new NotImplementedException(); }
}
public class HumanWorker : IWorker
{
public void Work() { }
public void Eat() { }
}
The Robot
class has to implement the Eat
method, even though it doesn't need it.
✔️ Interface Segregation Principle Compliant
public interface IWorkable
{
void Work();
}
public interface IFeedable
{
void Eat();
}
public class HumanWorker : IWorkable, IFeedable
{
public void Work() { }
public void Eat() { }
}
public class RobotWorker : IWorkable
{
public void Work() { }
}
Improvements:
Currently, the RobotWorker
class only implements the IWorkable
interface, avoiding the unnecessary Eat
method.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Dependency Inversion Principle Violation
public class Database
{
public void Save() { }
}
public class EmployeeManager
{
private Database _database;
public EmployeeManager()
{
_database = new Database();
}
public void SaveEmployee()
{
_database.Save();
}
}
The EmployeeManager
class depends on the concrete Database
class, making switching to another storage implementation difficult.
✔️ Dependency Inversion Principle Compliant
public interface IDataStore
{
void Save();
}
public class Database : IDataStore
{
public void Save() { }
}
public class EmployeeManager
{
private IDataStore _dataStore;
public EmployeeManager(IDataStore dataStore)
{
_dataStore = dataStore;
}
public void SaveEmployee()
{
_dataStore.Save();
}
}
Improvements:
Currently, EmployeeManager
depends on the abstraction IDataStore
. This allows us to use any class that implements IDataStore
without changing EmployeeManager
.
Key Takeaways
- Single Responsibility Principle: Each class should do one thing well.
- Open/Closed Principle: Make it easy to add new functionality without changing existing code.
- Liskov Substitution Principle: Subclasses should be substitutable for their base classes.
- Interface Segregation Principle: Avoid forcing classes to implement methods they don't need.
- Dependency Inversion Principle: Depend on abstractions rather than concrete implementations.
Understanding and implementing SOLID principles is crucial for writing robust, maintainable software. These principles will help you write cleaner, more organized code that’s easier for your team to understand and extend.
Bonus: