Skip to content

Following SOLID design principles

Principle · Chapter 6

SOLID is an acronym for five design principles that produce code that is more understandable, maintainable, reusable, testable, and flexible:

  • 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)

Treat them as guidelines, not absolutes — use judgment on when and how far to apply each.

  • Each class should have one responsibility — one reason to change. Group things that change for the same reason; separate things that change for different reasons.
  • A class with multiple responsibilities is used in more places, so a change risks introducing defects across responsibilities and impacts more classes.
  • SRP is how you build an orthogonal system at the class level; it relates to SoC, and DRY helps you achieve it.
  • Not the same as “one public method per class” — a single responsibility may need several methods.
  • Example: an EmailService that also writes to a log file has two reasons to change. Fix by abstracting logging behind an ILogger interface injected via the constructor, leaving EmailService responsible only for sending email.
  • Software components should be open for extension but closed for modification — add new behavior via new code without editing existing, working code.
  • Bertrand Meyer originally framed this via implementation inheritance; Robert C. Martin (“Uncle Bob”) reframed it around abstraction and interfaces — swap implementations without changing the code that depends on the interface.
  • Example: a Canvas.DrawShape with if (shape is Rectangle) … if (shape is Circle) … must be modified for every new shape (violates OCP). Refactor to an IShape interface with a Draw() method; each shape implements its own Draw(), and Canvas never changes when a new shape is added.
  • Subtypes must be substitutable for their base types without altering correctness — a subclass can stand in for its base class with no surprises.
  • Subtypes must not change the expected behavior of the base class; violations compile but cause unexpected behavior or runtime errors and confusing code.
  • Classic counterexample: Square inheriting from Rectangle. Overriding width/height so they stay equal breaks callers that set them independently — a rectangle expected to compute area 6 (3×2) returns 4. Square is not truly substitutable for Rectangle; such violations cause hard-to-find defects in complex hierarchies.
  • Clients should not be forced to depend on members they don’t use. Prefer small, cohesive interfaces; split large (“fat”) interfaces into focused ones.
  • Code smell: implementing an interface forces you to mark methods/properties as NotSupported/not implemented — a sign the interface should be segregated. Fat interfaces increase coupling and hurt maintainability.
  • Example: a Movie forced to implement IProduct.AuthorId (and needing an extra RunningTime) signals a bad interface. Refactor: base IProduct (shared members) with IBook : IProduct (adds AuthorId) and IMovie : IProduct (adds RunningTime).
  • “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.” (Robert & Micah Martin.)
  • Direct dependency graphs (A → B → C, each newing up its concrete dependency) are tightly coupled, hard to maintain, and not unit-testable — you can’t inject mocks for concrete types.
  • Invert it: high-level classes depend on interfaces; implementations depend on those interfaces. Enables loose coupling and true unit tests. Improves testability.
  • A design principle where reusable code (e.g. a framework) calls into your system, inverting the traditional flow where your system calls into a library.
  • Broader than dependencies, but strongly associated with them — which is why DI containers were historically called IoC containers.

A technique that supplies (injects) a class’s dependencies to achieve dependency inversion.

Benefits:

  • Removes hardcoded dependencies; implementation can be chosen at compile-time or runtime (late binding) as long as you program to an interface.
  • Loose coupling → easier to maintain, extend, and test.
  • Testability: depend on abstractions, so dependencies can be mocked in unit tests.
  • Parallel development: teams agree on shared interfaces and build implementations independently.

DI patterns:

PatternHow dependencies arriveNotes
Constructor injectionPassed through the constructorPreferred — dependencies are explicit; object can’t exist without them. Use guard clauses + readonly fields
Property injectionSet via a public propertyImplicit; provide a default via lazy initialization; can restrict to set-once
Method injectionPassed through a method (e.g. Initialize)Similar to property injection
Service LocatorA locator object resolves instances on demandConsidered an anti-pattern by many — it hides a class’s dependencies
  • DI containers (a.k.a. IoC containers) are frameworks that create and inject dependencies automatically. Not required to use DI, but they remove the manual “grunt work” once dependencies go several levels deep. Rolling your own automation effectively reinvents a container.
  • Software Architect’s Handbook (Packt, 2018), Ch.6 “Following SOLID design principles”, pp. 437-460.