Solid Principles
SOLID Principles: A Comprehensive Explanation
SOLID principles are the foundation of robust, maintainable, and scalable software design. These five object-oriented design principles guide developers to write code that is easy to understand, extend, and refactor.
1. Single Responsibility Principle (SRP)
Definition:
A class should have only one reason to change, meaning it should have a single responsibility or purpose.
Problem:
When a class has multiple responsibilities, changes in one responsibility can inadvertently affect the other, leading to tightly coupled code.
Example (Violation):
public class EmailManager
{
public void SendEmail(string message) { /* Logic to send email */ }
public void LogEmail(string message) { /* Logic to log email */ }
}
Why It’s a Problem:
The class is responsible for both sending and logging emails. Any change in the logging mechanism could impact email-sending functionality.
Solution:
public class EmailSender
{
public void SendEmail(string message) { /* Logic to send email */ }
}
public class EmailLogger
{
public void LogEmail(string message) { /* Logic to log email */ }
}
2. Open/Closed Principle (OCP)
Definition:
Classes should be open for extension but closed for modification.
Problem:
Modifying existing classes to add new behavior can introduce bugs and violate existing functionality.
Example (Violation):
public class Shape
{
public string Type { get; set; }
public double CalculateArea()
{
if (Type == "Circle") return Math.PI * 5 * 5; // Hardcoded radius
if (Type == "Rectangle") return 10 * 5; // Hardcoded dimensions
return 0;
}
}
Why It’s a Problem:
Adding new shapes requires modifying the CalculateArea
method, breaking the OCP.
Solution:
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea() => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
3. Liskov Substitution Principle (LSP)
Definition:
Derived classes should be substitutable for their base classes without altering the program's behavior.
Problem:
When a derived class overrides or changes behavior in ways that break expectations of the base class, it violates LSP.
Example (Violation):
public class Bird
{
public virtual void Fly() { /* Logic to fly */ }
}
public class Penguin : Bird
{
public override void Fly() { throw new NotImplementedException(); }
}
Why It’s a Problem:
A Penguin
is a Bird
but cannot fly, violating the LSP as it breaks the behavior expected of Bird
.
Solution:
public abstract class Bird { }
public class FlyingBird : Bird
{
public void Fly() { /* Logic to fly */ }
}
public class Penguin : Bird
{
public void Swim() { /* Logic to swim */ }
}
4. Interface Segregation Principle (ISP)
Definition:
A class should not be forced to implement interfaces it does not use. Instead, create smaller, more specific interfaces.
Problem:
A large interface can force classes to implement methods they do not need, leading to unnecessary or empty implementations.
Example (Violation):
public interface IWorker
{
void Work();
void Eat();
}
public class Robot : IWorker
{
public void Work() { /* Robot working logic */ }
public void Eat() { throw new NotImplementedException(); }
}
Why It’s a Problem:
The Robot
class does not need to implement Eat()
but is forced to, resulting in an exception.
Solution:
public interface IWorker
{
void Work();
}
public interface IEater
{
void Eat();
}
public class Robot : IWorker
{
public void Work() { /* Robot working logic */ }
}
5. Dependency Inversion Principle (DIP)
Definition:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Problem:
Direct dependencies on concrete implementations make the code less flexible and harder to test.
Example (Violation):
public class EmailSender
{
public void Send(string message) { /* Email sending logic */ }
}
public class Notification
{
private EmailSender emailSender = new EmailSender();
public void Notify(string message)
{
emailSender.Send(message);
}
}
Why It’s a Problem:
The Notification
class depends directly on the EmailSender
, making it hard to replace or test.
Solution:
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) { /* Email sending logic */ }
}
public class Notification
{
private readonly IMessageSender _messageSender;
public Notification(IMessageSender messageSender)
{
_messageSender = messageSender;
}
public void Notify(string message)
{
_messageSender.Send(message);
}
}
Summary Table
Principle | Definition | Key Issue | Solution Example |
SRP (Single Responsibility) | A class should have only one responsibility. | Tightly coupled responsibilities | Split responsibilities into classes |
OCP (Open/Closed) | Open for extension, closed for modification. | Hardcoding behavior in a class | Use abstractions or polymorphism |
LSP (Liskov Substitution) | Subclasses must replace base classes without altering behavior. | Violating expected behavior of a class | Refactor class hierarchy |
ISP (Interface Segregation) | Do not force classes to implement methods they do not use. | Large, bloated interfaces | Create smaller, specific interfaces |
DIP (Dependency Inversion) | High-level modules should depend on abstractions, not concrete implementations. | Tight coupling between classes | Use interfaces and dependency injection |
Conclusion
By following the SOLID principles, you can create software that is easy to understand, extend, and maintain. These principles promote good design practices and ensure your codebase remains adaptable to changing requirements.