Solid Principles Explained

The SOLID principles are a set of design guidelines in object-oriented programming that enable developers to build more maintainable and scalable software. These principles were introduced by Robert C. Martin (Uncle Bob) in his 2000 paper "Design Principles and Design Patterns."

The acronym SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Each principle helps developers to manage dependencies, which in turn reduces the complexity of the code and makes it easier to manage over time.

Learn more about the paper clicking here

1. Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should have only one job or responsibility.

C#
// Incorrect: Combining logging and processing responsibilities
public class OrderHandler
{
    public void ProcessOrder(Order order)
    {
        // Process the order
    }

    public void LogOrder(Order order)
    {
        // Log order details
    }
}
    
C#
// Correct: Separate classes for processing and logging
public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // Process the order
    }
}

public class OrderLogger
{
    public void LogOrder(Order order)
    {
        // Log order details
    }
}
    

2. Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

C#
// Incorrect: Modifying existing class to add new features
public class Discount
{
    public double CalculateDiscount(string type, double price)
    {
        if (type == "Seasonal")
        {
            return price * 0.15;
        }
        else if (type == "Veteran")
        {
            return price * 0.1;
        }
        return 0;
    }
}
    
C#
// Correct: Using abstract class to allow extensions without modifying existing code
public abstract class Discount
{
    public abstract double CalculateDiscount(double productPrice);
}

public class SeasonalDiscount : Discount
{
    public override double CalculateDiscount(double productPrice)
    {
        return productPrice * 0.15;
    }
}

public class VeteranDiscount : Discount
{
    public override double CalculateDiscount(double productPrice)
    {
        return productPrice * 0.1;
    }
}
    

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

C#
// Incorrect: Subclass that breaks functionality of the superclass
public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }

    public int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public new int Width
    {
        set { base.Width = base.Height = value; }
    }

    public new int Height
    {
        set { base.Height = base.Width = value; }
    }
}
    
C#
// Correct: Refactoring to avoid breaking LSP
public class Shape
{
    public virtual int CalculateArea() { return 0; }
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Shape
{
    public int SideLength { get; set; }

    public override int CalculateArea()
    {
        return SideLength * SideLength;
    }
}
    

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

C#
// Incorrect: One interface for all functionalities
public interface IMachine
{
    void Print(Document d);
    void Scan(Document d);
    void Fax(Document d);
}

public class SimplePrinter : IMachine
{
    public void Print(Document d) { /* Implement printing */ }
    public void Scan(Document d) { /* Not used */ }
    public void Fax(Document d) { /* Not used */ }
}
    
C#
// Correct: Segregated interfaces for specific functionalities
public interface IPrinter
{
    void Print(Document d);
}

public interface IScanner
{
    void Scan(Document d);
}

public class SimplePrinter : IPrinter
{
    public void Print(Document d)
    {
        // Implement printing
    }
}
    

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

C#
// Incorrect: High-level module directly depends on low-level module
public class DataManager
{
    private FileDataAccess _dataAccess = new FileDataAccess();

    public void Load()
    {
        _dataAccess.LoadData();
    }

    public void Save()
    {
        _dataAccess.SaveData();
    }
}
    
C#
// Correct: High-level module depends on abstraction, not on concretions
public interface IDataAccess
{
    void LoadData();
    void SaveData();
}

public class FileDataAccess : IDataAccess
{
    public void LoadData()
    {
        // Load data from a file
    }

    public void SaveData()
    {
        // Save data to a file
    }
}

public class DataManager
{
    private IDataAccess _dataAccess;

    public DataManager(IDataAccess dataAccess)
    {
        _dataAccess = dataAccess;
    }

    public void Load()
    {
        _dataAccess.LoadData();
    }

    public void Save()
    {
        _dataAccess.SaveData();
    }
}