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

PrincipleDefinitionKey IssueSolution Example
SRP (Single Responsibility)A class should have only one responsibility.Tightly coupled responsibilitiesSplit responsibilities into classes
OCP (Open/Closed)Open for extension, closed for modification.Hardcoding behavior in a classUse abstractions or polymorphism
LSP (Liskov Substitution)Subclasses must replace base classes without altering behavior.Violating expected behavior of a classRefactor class hierarchy
ISP (Interface Segregation)Do not force classes to implement methods they do not use.Large, bloated interfacesCreate smaller, specific interfaces
DIP (Dependency Inversion)High-level modules should depend on abstractions, not concrete implementations.Tight coupling between classesUse 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.