SOLID Principles

Why Design Principles?

  • Software evolves: teams, requirements, and deadlines change.

  • Codebases grow into massive systems.

  • Small design mistakes lead to long-term maintenance issues.

  • Systems become rigid, fragile, and expensive to change.

  • Without good design:

    • Adding features becomes dangerous.

    • Fixing bugs introduces new ones.

    • Developers fear touching old code.

Symptoms of Bad Design

  • Rigidity: Difficult to change.

  • Fragility: Easy to break.

  • Immobility: Hard to reuse.

  • Viscosity: Easier to hack than do it properly.

  • Opacity: Hard to understand the code.

  • Design rot is real, and SOLID helps fight it!

Real-World Analogy: Restaurant Kitchen

  • Bad Design:

    • Ingredients thrown into one fridge.

    • Recipes are handwritten differently every day.

    • Everyone shouts for ingredients.

    • Results:

      • Meals take forever.

      • Orders are wrong.

      • Unhappy customers.

  • Good Design:

    • Ingredients are labeled and stored correctly.

    • Recipes are standardized.

    • Chefs know their roles.

    • Good Code:

      • Clear organization.

      • Responsibilities are divided.

      • Everyone (and every object) knows their role.

  • SOLID principles = Recipes for building a clean kitchen for your code!

A Brief History of SOLID

  • Introduced by Robert C. Martin (Uncle Bob) around 2000.

  • Named and popularized by Michael Feathers.

  • Built on decades of object-oriented design experience.

  • Focused on writing code that is flexible, extensible, and maintainable.

  • Today:

    • SOLID principles are a foundation for Agile development.

    • Critical for Test-Driven Development (TDD), Design Patterns, and Clean Architecture.

The Five SOLID Principles

  • SOLID is an acronym:

    • S – Single Responsibility Principle (SRP)

    • O – Open/Closed Principle (OCP)

    • L – Liskov Substitution Principle (LSP)

    • I – Interface Segregation Principle (ISP)

    • D – Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

  • Official Definition: "A class should have one, and only one, reason to change."

    • Robert C. Martin ("Uncle Bob"), Agile Software Development.

    • Classes with multiple responsibilities are more fragile and harder to maintain.

  • Each class should only do one job.

  • If a class handles many jobs, changing one job might break another.

  • One actor, one responsibility.

Real-World Analogy: The Actor Principle

  • In a play, each actor plays one character.

  • A class should be responsible for just one role; don't mix unrelated responsibilities.

Bad Example: Employee Class

class Employee {
    void calculatePay() { ... }
    void saveToDatabase() { ... }
    void generateReport() { ... }
}
  • Problem: Business logic (salary calculation), persistence logic (database save), and presentation logic (report generation) are all mixed together.

Problems When SRP is Violated

  • Tight Coupling: Change in database affects business logic.

  • Higher Risk: Bug fixes introduce unexpected side-effects.

  • Hard Testing: Unit testing is complicated because responsibilities are tangled.

  • Hard Reuse: You can’t reuse parts without dragging unrelated code.

  • SRP violation leads to fragile, tangled systems.

How to Apply SRP

  • Split responsibilities into separate classes:

    • One class per role.

    • Clear boundaries of responsibility.

    • Changes in one area don’t affect others.

  • Design Tip: Whenever you see ”and” in a class description, it probably needs a split!

Good Example: Separate Classes

class Employee { ... }

class PayCalculator {
    double calculatePay(Employee e) { ... }
}

class EmployeeRepository {
    void save(Employee e) { ... }
}

class EmployeeReport {
    void generate(Employee e) { ... }
}
  • Each class now has one reason to change.

Checklist: Signs SRP Might Be Violated

  • Class does many unrelated things.

  • Class grows too large over time (”God Class”).

  • Class touches too many external systems (DB, UI, Email, etc.).

  • Multiple teams need to modify the same class for different reasons.

  • When in doubt: Split it out!

Mini-Quiz - SRP Practice

  • Which classes violate SRP? (Select all that apply)

    • A class that manages database connections and user sessions.

    • A class that only calculates invoice totals.

    • A class that reads files, writes to network, and processes payments.

    • A class that validates email formats.

  • Answer: 1 and 3 violate SRP.

Open/Closed Principle (OCP)

  • Official Definition: ”Software entities should be open for extension, but closed for modification.”

  • Meaning: Add new behavior without changing existing code.

  • Protect working code from being broken by changes.

  • Extend the behavior of a class without altering its source code.

  • Existing tested code should stay untouched as much as possible.

  • Build systems like LEGO blocks — add more pieces without reshaping old ones!

Real-World Analogy: Power Socket Extensions

  • Wall sockets are ”closed” — you don’t modify the wall wiring.

  • But you can ”extend” functionality — plug in extension cords, splitters, adapters.

  • In Code: Core modules stay stable. New features plug in without breaking old ones.

Bad Example: Graphic Editor with Switch Case

class GraphicEditor {
    void drawShape(Shape s) {
        if (s.type == ”Circle”) drawCircle(s);
        else if (s.type == ”Square”) drawSquare(s);
    }
}
  • Adding new shapes requires editing this method — violating OCP!

Problems When OCP is Violated

  • Fragile: changing switch logic may break unrelated shapes.

  • Risky: one typo could crash the system.

  • Inefficient: recompile and retest old working code every time.

  • Bottleneck: one team must control all modifications.

  • Bad for scalability and team growth.

How to Apply OCP

  • Use polymorphism:

    • Create an interface or abstract class.

    • Let each new behavior implement or inherit.

    • Core classes call the abstract methods—don't care about details.

  • This way, new behaviors come as new classes — no modification needed.

Good Example: Using Polymorphism

interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() { /* draw circle */ }
}

class Square implements Shape {
    public void draw() { /* draw square */ }
}

class GraphicEditor {
    void drawShape(Shape s) {
        s.draw();
    }
}
  • New shapes extend the system—without touching GraphicEditor!

Checklist: Signs OCP Might Be Violated

  • Frequent modifications to stable code when adding new features.

  • Growing long ”if-else” or ”switch-case” chains.

  • Risk of introducing bugs in old logic when adding new options.

  • New types require editing core processing classes.

  • When in doubt: Abstract it out!

Mini-Quiz — OCP Practice

  • Which design follows OCP?

    • A switch-case for every new payment method (Visa, MasterCard, PayPal).

    • A Payment interface with classes for Visa, MasterCard, PayPal implementing it.

    • A hardcoded if-else for each notification type (Email, SMS, Push).

  • Answer: 2 follows OCP.

Liskov Substitution Principle (LSP)

  • Official Definition: ”Subtypes must be substitutable for their base types without altering desirable behavior.”

    • Introduced by: Barbara Liskov, 1987.

  • If code works with a base class, it should also work with any derived class—without surprises.

  • Subclasses must honor the promises made by their parents.

  • Child objects should behave like parents—or better.

Real-World Analogy: Vehicle Rental

  • You rent a ”vehicle” expecting to drive.

  • If the rental company gives you a ”boat” instead, you’re stuck!

  • In Code: Substituting types should not break client expectations.

Bad Example: Rectangle and Square

class Rectangle {
    void setWidth(int w) { ... }
    void setHeight(int h) { ... }
}

class Square extends Rectangle {
    void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w);
    }
    void setHeight(int h) {
        super.setWidth(h);
        super.setHeight(h);
    }
}
  • Square ”breaks” the behavior expected of Rectangle clients.

Problems When LSP is Violated

  • Clients get unpredictable behavior.

  • Testing becomes complicated.

  • Bugs are introduced silently—harder to detect.

  • Violation destroys confidence in type hierarchy!

How to Apply LSP

  • Redesign the hierarchy carefully.

  • Do not use inheritance if behavior cannot be preserved.

  • Prefer interfaces or separate abstractions if substitution doesn’t make sense.

  • Tip: Inheritance = ”is-a” relationship—check it carefully.

Good Example: No Forced Inheritance

interface Shape {
    int area();
}

class Rectangle implements Shape {
    int width, height;
    int area() {
        return width * height;
    }
}

class Square implements Shape {
    int side;
    int area() {
        return side * side;
    }
}
  • Both shapes implement the same contract without confusion.

Checklist: Signs LSP Might Be Violated

  • Subclass overrides methods and changes expected behavior.

  • Subclass throws unexpected exceptions.

  • Clients must add ”instanceof” checks to distinguish subclasses.

  • When in doubt: Flatten the hierarchy!

Mini-Quiz — LSP Practice

  • Which one violates LSP?

    • A Dog class extends Animal and behaves correctly.

    • A Duck class extends Bird but cannot fly (throws exception when fly() is called).

    • A Car class implements Driveable and drive s safely.

  • Answer: 2 violates LSP.

Interface Segregation Principle (ISP)

  • Official Definition: ”Clients should not be forced to depend upon interfaces that they do not use.”

  • Meaning: Prefer many small, focused interfaces over one large general-purpose interface.

  • Clients should only know about the methods that are relevant to them.

  • Don’t burden classes with unnecessary obligations.

  • Don’t make a printer implement ”fax” if it can’t fax!

Real-World Analogy: Restaurant Menu

  • Imagine a vegetarian ordering from a menu that forces them to choose a meat dish.

  • Very confusing and annoying!

  • In Code: Clients should only see the methods they need.

Bad Example: Multi-Function Interface

interface Machine {
    void print();
    void scan();
    void fax();
}

class BasicPrinter implements Machine {
    public void print() { ... }
    public void scan() {
        throw new UnsupportedOperationException();
    }
    public void fax() {
        throw new UnsupportedOperationException();
    }
}
  • Classes are forced to implement methods they don’t support!

Problems When ISP is Violated

  • Unnecessary code complexity.

  • Higher risk of runtime errors (e.g., unsupported methods).

  • Harder to understand and maintain.

  • Break interfaces into smaller, focused roles!

How to Apply ISP

  • Split interfaces by responsibilities:

    • One interface per logical group.

    • Classes implement only what they actually support.

  • Example: Printer, Scanner, and Fax interfaces separately.

Good Example: Focused Interfaces

interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

interface Fax {
    void fax();
}

class BasicPrinter implements Printer {
    public void print() { ... }
}
  • Classes now only implement what they truly offer!

Checklist: Signs ISP Might Be Violated

  • Classes throw exceptions for unimplemented methods.

  • Interfaces seem bloated or unrelated.

  • Clients know too much about unrelated behavior.

  • When in doubt: Split it out!

Mini-Quiz — ISP Practice

  • Which option follows ISP?

    • A Device interface with print(), scan(), fax(), copy() methods.

    • Separate Printer, Scanner, Copier interfaces.

  • Answer: 2 follows ISP.

Dependency Inversion Principle (DIP)

  • Official Definition: ”High-level modules should not depend on low-level modules. Both should depend on abstractions.”

  • Also: ”Abstractions should not depend on details. Details should depend on abstractions.”

  • Code should depend on interfaces, not implementations.

  • High-level logic (business rules) should not know how low-level parts (e.g., database, email) work.

  • Don’t hardcode dependencies; inject flexibility.

Real-World Analogy: Universal Power Adapters

  • Travelers use universal adapters — they work regardless of country.

  • You plug into a standard interface, not directly into the wall’s wiring.

  • In Code: Design against interfaces, not hardcoded classes.

Bad Example: Hardcoded Dependency

class EmailSender {
    void send(String message) { ... }
}

class OrderService {
    private EmailSender sender = new EmailSender();

    void completeOrder() {
        sender.send(”Order completed”);
    }
}
  • Problem: Can’t reuse OrderService with a different message sender. Hard to test (no mocks).

Problems When DIP is Violated

  • Low-level changes ripple into high-level business logic.

  • Difficult to replace components (e.g., swap database or logger).

  • Harder to test in isolation.

  • Leads to tight coupling between layers.

  • Abstract away your dependencies!

How to Apply DIP

  • Solution: Introduce an interface or abstract base class.

  • Make high-level modules depend on that abstraction.

  • Inject the concrete implementation at runtime (via constructor).

  • Use with: IoC, Dependency Injection, Mocking for tests.

Good Example: Abstracted Dependency

interface Notifier {
    void send(String message);
}

class EmailSender implements Notifier {
    public void send(String message) { ... }
}

class OrderService {
    private final Notifier notifier;

    OrderService(Notifier notifier) {
        this.notifier = notifier;
    }

    void completeOrder() {
        notifier.send(”Order completed”);
    }
}

Checklist: Signs DIP Might Be Violated

  • Business logic instantiates low-level classes directly.

  • Cannot easily switch implementations (e.g., File → DB → Cloud).

  • Unit tests are difficult due to hardwired dependencies.

  • Code breaks if low-level modules change their details.

  • When in doubt: Invert the dependency!

Mini-Quiz — DIP Practice

  • Which example follows DIP?

    • LoggerService creates a FileLogger directly in its constructor.

    • LoggerService accepts an ILogger interface via constructor.

  • Answer: 2 follows DIP.

SOLID — Quick Recap

  • S — Single Responsibility Principle: Each class should have only one reason to change.

  • O — Open/Closed Principle: Classes should be open for extension, closed for modification.

  • L — Liskov Substitution Principle: Subtypes must be usable in place of their supertypes without altering behavior.

  • I — Interface Segregation Principle: Many client-specific interfaces are better than one general-purpose interface.

  • D — Dependency Inversion Principle: Depend on abstractions, not concretions.

Final Key Messages

  • Good design is deliberate, not accidental.

  • SOLID principles are guidelines, not rigid rules.

  • Prioritize clarity, simplicity, and separation of concerns.

  • Mastering SOLID leads to flexible, robust, and scalable systems.

  • Design today what you’ll be proud to maintain tomorrow!

Quick Quiz — Test Your Understanding

  • Which principle aims to minimize ”God Classes”? (Answer: SRP)

  • Which principle says software should be extendable without modifying existing code? (Answer: OCP)

  • Violating which principle leads to runtime surprises when substituting types? (Answer: LSP)

  • Which principle promotes using smaller, focused interfaces? (Answer: ISP)

  • Which principle recommends depending on interfaces rather than implementations? (Answer: DIP)

Discussion Questions

  • Give a real-world example where SRP violation caused a problem.

  • How would OCP help in building a plugin system?

  • How could LSP violations cause subtle bugs in polymorphic collections?

Mini Case Studies

  • SRP: Class InvoiceManager handles invoice calculations, database saving, PDF generation, and emailing invoices.

    • Identify how SRP is violated and suggest a refactoring.

  • OCP: NotificationService uses a huge if-else ladder to send emails, SMS, and push notifications.

    • How is OCP violated? How could you extend this system without modifying the service?

  • LSP: A Bird class has a fly() method. A Penguin subclass throws an exception when fly() is called.

    • Why is LSP violated? How could you redesign this?

  • ISP: A SmartDevice interface includes call(), browseInternet(), playGames(), but a basic feature-phone only supports calling.

    • How is ISP violated? How would you redesign the interfaces?

  • DIP: An OrderProcessor creates a MySQLDatabase object directly inside its methods.

    • Why does this violate DIP? Suggest a better design.