01

SOLID — briefly, with an honest assessment

#
  • heuristic
  • opinionated
  • fragment

Scope. ISP and DIP also yield testable failure modes, so the table’s verdicts are a ranking, not a proof that the other four are untestable.

Conflicts with: OCP ↔ YAGNI

Primary sources: Robert C. Martin — The Single Responsibility Principle (the actor-based formulation)

Other claims on this card, typed separately:

    • author’s position
    • opinionated

    The verdicts in the table, and “LSP is the only one worth examples”, are this page’s ranking, not a consensus and not a property of SOLID.

PrincipleGistVerdict
SRPOne reason to change (one stakeholder)Not operationalizable; rhetoric
OCPExtend with new code, not by editing old codeConflicts with YAGNI when applied prematurely
LSPA subtype is substitutable without breaking the contractThe only verifiable one; violations are real bugs
ISPA client does not depend on methods it doesn't usePortable anywhere; useful
DIPDependencies point toward the abstraction; the interface belongs to the consumerUseful, but degenerates into single-implementation interfaces

A “reason to change” is people, not requirements. Martin's later formulation: a module is responsible to one, and only one, actor — a group of people who originate requirements. A class that stores a shopping list and computes its cost violates SRP not because it “does two things”, but because different people own the list's contents and the pricing rules, and their requirements change independently. The actor is a criterion for where to cut, not a design driver: you don't create one class per stakeholder; you check the converse — that no single artifact serves two masters with mismatched change schedules.

None of this makes it operationalizable: granularity is arbitrary, the principle gives no stopping criterion for decomposition — a violation is recognized after the fact, and no design can be derived from it. The practical test is file history: commits from unrelated tasks by different people systematically converging on one class. The guard against the opposite extreme, splitting for the sake of splitting, is cohesion (13): what changes together lives together. The operationalizable replacement is information hiding (12): the question is not “how many responsibilities are here” but “what breaks outside if this internal decision is revisited”.

A contract has two layers. Signatures — which methods, which types — are checked by the compiler. Behavior — what a method promises to do — is checked by nobody but reviews and tests. LSP requires holding both: a subtype that matches the signatures but breaks the promise breaks everyone who wrote code against the supertype. The canonical example: Rectangle has setWidth and setHeight; Square, inheriting from it, must keep its sides equal — its setWidth(5) quietly changes the height too, and code that relied on “set the width, the height stays untouched” breaks precisely on the square. The compiler is happy; the contract is broken. The cure in the examples below is not to fix the inheritance but to remove it: a closed set of variants with no mutating setWidth (3), plus contract tests run against every implementation.

An LSP violation and its fix: the only SOLID principle that deserves examples.

Code example (fragment)
fragment
Java
// sealed instead of inheritance — the contract is closed and compiler-checked
sealed interface Shape permits Rect, Square {}

record Rect(double w, double h) implements Shape {}
record Square(double side) implements Shape {}

// Square extends Rectangle is impossible by construction — no mutating setWidth
Rust
// LSP is expressed through trait contracts; a violation is a panic instead of Err
trait Storage {
    fn get(&self, k: &str) -> Result<Option<Vec<u8>>, StorageErr>; // contract: never panics
}

// an impl that calls unwrap() inside get violates LSP,
// caught by code review + proptest
Go
// io.Reader's contract lives in its docs; an implementation that panics instead of
// returning an error violates LSP, even though the compiler is happy
type SafeReader struct{ inner io.Reader }

func (r SafeReader) Read(p []byte) (int, error) {
    n, err := r.inner.Read(p)
    if err != nil && err != io.EOF {
        return n, fmt.Errorf("read: %w", err) // not panic
    }
    return n, err
}
Python
# Protocol + a contract test run against every implementation
from typing import Protocol

class Storage(Protocol):
    def get(self, key: str) -> bytes | None: ...

def storage_contract_suite(impl: Storage):   # one test suite for all implementations
    assert impl.get("missing") is None       # not KeyError — that's the contract

Last technical review: 2026-08-04 · Reference · 112 principles