Design Patterns & Practices  

SOLID Principles Cheat Sheet 🚀

Quick Memory Aid: "SOLID"

  • S - Single Responsibility Principle

  • O - Open/Closed Principle

  • L - Liskov Substitution Principle

  • I - Interface Segregation Principle

  • D - Dependency Inversion Principle

1. Single Responsibility Principle (SRP) 📝

"One Class, One Job"

Official Definition: A class should have only one reason to change, meaning that a class should have only one job or responsibility.

Rule: A class should have only one reason to change.

❌ Bad Example:

public class Employee{
    public void SaveEmployee() { /* Save to database */ }
    public void SendEmail() { /* Send email notification */ }
    public void GenerateReport() { /* Generate PDF report */ }
}// Problem: 3 different reasons to change this class!

✅ Good Example:

public class Employee{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

public class EmployeeRepository{
    public void SaveEmployee(Employee emp) { /* Save logic */ }
}

public class EmailService{
    public void SendEmail(string message) { /* Email logic */ }
}

public class ReportGenerator{
    public void GenerateReport(Employee emp) { /* Report logic */ }
}

Memory Tip: "Single = Solo = One job per class"

2. Open/Closed Principle (OCP) 🔓🔒

"Open for Extension, Closed for Modification"

Official Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.

Rule: Classes should be open for extension but closed for modification.

❌ Bad Example:

public class AreaCalculator{
    public double CalculateArea(object shape)
    {
        if (shape is Rectangle rect)
            return rect.Width * rect.Height;
        else if (shape is Circle circle)
            return Math.PI * circle.Radius * circle.Radius;
        // Need to modify this method for new shapes!
        return 0;
    }
}

✅ Good Example:

public abstract class Shape{
    public abstract double CalculateArea();
}

public class Rectangle : Shape{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea() => Width * Height;
}

public class Circle : Shape{
    public double Radius { get; set; }
    
    public override double CalculateArea() => Math.PI * Radius * Radius;
}

public class AreaCalculator{
    public double CalculateArea(Shape shape) => shape.CalculateArea();
    // No need to modify for new shapes!
}

Memory Tip: "Open door for new features, Closed door for changes"

3. Liskov Substitution Principle (LSP) 🔄

"Subclasses Must Be Substitutable"

Official Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

Rule: Objects of a superclass should be replaceable with objects of a subclass without breaking functionality.

❌ Bad Example:

public class Bird{
    public virtual void Fly() => Console.WriteLine("Flying...");}

public class Penguin : Bird{
    public override void Fly() => throw new Exception("Can't fly!");
    // Breaks LSP - penguin can't substitute bird behavior}

✅ Good Example:

public abstract class Bird{
    public abstract void Move();}

public class FlyingBird : Bird{
    public override void Move() => Console.WriteLine("Flying...");}

public class SwimmingBird : Bird{
    public override void Move() => Console.WriteLine("Swimming...");}

public class Eagle : FlyingBird { }public class Penguin : SwimmingBird { }

Memory Tip: "Lisa (Liskov) can substitute any family member in photos"

4. Interface Segregation Principle (ISP) ✂️

"Many Small Interfaces > One Big Interface"

Official Definition: No client should be forced to depend on methods it does not use. This principle suggests that it's better to have many specific interfaces rather than one general-purpose interface.

Rule: Clients should not be forced to depend on interfaces they don't use.

❌ Bad Example:

public interface IWorker{
    void Work();
    void Eat();
    void Sleep();}

public class Robot : IWorker{
    public void Work() => Console.WriteLine("Working...");
    public void Eat() => throw new NotImplementedException(); // Robot doesn't eat!
    public void Sleep() => throw new NotImplementedException(); // Robot doesn't sleep!}

✅ Good Example:

public interface IWorkable{
    void Work();}

public interface IFeedable{
    void Eat();}

public interface ISleepable{
    void Sleep();}

public class Human : IWorkable, IFeedable, ISleepable{
    public void Work() => Console.WriteLine("Working...");
    public void Eat() => Console.WriteLine("Eating...");
    public void Sleep() => Console.WriteLine("Sleeping...");}

public class Robot : IWorkable{
    public void Work() => Console.WriteLine("Working...");
    // Only implements what it needs!}

Memory Tip: "Interface Separation = Split interfaces like pizza slices"

5. Dependency Inversion Principle (DIP) 🔃

"Depend on Abstractions, Not Concretions"

Official Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

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

❌ Bad Example:

public class EmailService{
    public void SendEmail(string message) { /* Send email */ }}

public class NotificationService{
    private EmailService emailService = new EmailService(); // Tight coupling!
    
    public void SendNotification(string message)
    {
        emailService.SendEmail(message);
    }}

✅ Good Example:

public interface IMessageService{
    void SendMessage(string message);}

public class EmailService : IMessageService{
    public void SendMessage(string message) => Console.WriteLine($"Email: {message}");}

public class SMSService : IMessageService{
    public void SendMessage(string message) => Console.WriteLine($"SMS: {message}");}

public class NotificationService{
    private readonly IMessageService messageService;
    
    public NotificationService(IMessageService messageService)
    {
        this.messageService = messageService; // Dependency injection!
    }
    
    public void SendNotification(string message)
    {
        messageService.SendMessage(message);
    }}

Memory Tip: "Depend on Interface (abstract), not Implementation (concrete)"

🎯 Quick Reference Summary

PrincipleSimple RuleMemory Hook
SRPOne class, one jobSingle = Solo
OCPExtend, don't modifyOpen door, closed door
LSPChild can replace parentLisa substitutes family
ISPSmall focused interfacesSplit like pizza slices
DIPDepend on abstractionsInterface over Implementation

🏆 Benefits of Following SOLID:

  • ✅ Maintainable code

  • ✅ Testable components

  • ✅ Flexible architecture

  • ✅ Reusable modules

  • ✅ Scalable applications