Files
playbook/antigravity-awesome-skills/skills/clean-code-guard/references/solid.md
T
2026-07-18 00:02:59 +00:00

281 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SOLID — the five principles
Source: Robert C. Martin. The five principles were collected on Uncle Bob's "Principles of OOD" page on objectmentor.com (mirrored at butunclebob.com) and updated on blog.cleancoder.com. Original papers from *C++ Report* circa 19951996.
## Contents
- S: Single Responsibility Principle
- O: Open/Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle
- How AI-generated code typically breaks SOLID
- Self-check for SOLID
---
## S — Single Responsibility Principle
**Definition (Martin 2014, hardened from the original).** *"A module should be responsible to one, and only one, actor."*
Older form: "A class should have only one reason to change."
Source: blog.cleancoder.com — SRP, 2014.
### Why
The axis is *people*. Different stakeholders (Accounting, HR, DBA) want different things from the same class. When their needs change, they edit the same file, conflict, and break each other.
### Smells to flag
- One class contains methods touching unrelated subsystems (persistence + presentation + business rules).
- Methods on the class serve disjoint stakeholder groups.
- Git history shows two distinct clusters of co-changing methods inside one class.
### Common misinterpretation
*"A class should do one thing."* No. SRP is about **cohesion around an actor**, not method count. A 12-method `InvoiceRepository` answerable only to the data-access layer satisfies SRP. A 3-method class with one HTTP call, one Jinja render, and one DB write does not.
### Bad
```text
EmployeeReport
calculatePay() // Accounting
reportHours() // HR
save() // Data storage owner
```
### Good
```text
PayCalculator // Accounting
HoursReporter // HR
EmployeeRepository // Data storage owner
```
---
## O — Open/Closed Principle
**Definition.** *"Software entities (classes, modules, functions) should be open for extension, but closed for modification."*
Originally Bertrand Meyer (1988, *Object-Oriented Software Construction*); Martin refocused it on polymorphic abstraction rather than implementation inheritance.
Source: blog.cleancoder.com — OCP, 2014; Martin's 1996 paper PDF (Duke mirror).
### Why
Protect stable high-level policy from churn in low-level variants. New behavior should arrive as new code, not edits to working code.
### Smells to flag
- Branch dispatching on a type tag or runtime type check — every new type requires editing the same function.
- Adding a feature requires modifying N existing files instead of adding one.
- `match`/`enum` switches that cross module boundaries (policy reaching into details).
### Common misinterpretation
*"Never modify code."* The principle is that *modules containing high-level policy* should not be modified to accommodate new variants. Leaf code changes freely.
### Bad
```text
export(record, kind):
if kind == "pdf": return toPdf(record)
if kind == "csv": return toCsv(record)
if kind == "json": return toJson(record)
// adding "xml" requires editing this function
```
### Good
```text
exporters = {
"pdf": toPdf,
"csv": toCsv,
"json": toJson,
}
export(record, kind):
return exporters[kind](record)
// adding "xml" is one line in the table
```
---
## L — Liskov Substitution Principle
**Definition (Liskov & Wing, 1994).** *"If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T."*
Source: Martin's LSP paper PDF (LaBRI mirror).
### Why
Substitutability. Callers written against a base type must continue to work when handed a subtype — otherwise polymorphism leaks abstraction.
### Smells to flag
- A subclass overrides a method to signal "not implemented" or "unsupported operation."
- A subclass **strengthens preconditions** (rejects inputs the parent accepts).
- A subclass **weakens postconditions** (returns something the parent guarantees against).
- Callers perform runtime subtype checks to decide whether to call a method.
### Common misinterpretation
*"Subclasses must have the same methods."* That's signature compatibility, which is just type-checking. LSP is **behavioral**:
- Preconditions can only **weaken** in the subtype.
- Postconditions and invariants can only **strengthen**.
- Parameter types are contravariant; return types covariant.
### The Rectangle/Square classic
```text
Rectangle
setWidth(width)
setHeight(height)
area()
Square extends Rectangle
setWidth(width):
this.width = width
this.height = width // invariant: width == height
setHeight(height):
this.width = height // invariant: width == height
this.height = height
```
A caller holding a `Rectangle` reference does `r.set_width(5); r.set_height(4); assert r.area() == 20`. With a `Square`, the assertion fails — LSP violated.
The fix is *not* to fix the methods. The fix is that `Square is-not-a Rectangle` in the behavioral sense. Compose, don't inherit.
---
## I — Interface Segregation Principle
**Definition.** *"Clients should not be forced to depend on methods they do not use."* Equivalently: many client-specific interfaces beat one general-purpose interface.
Source: Martin's 1996 ISP paper, catalogued at butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod.
### Why
Fat interfaces create transitive coupling. Clients are dragged into recompiles and test-fixtures for methods they never call.
### Smells to flag
- A `Service` / `Manager` / `Repository` interface with 10+ methods, where any given caller uses one or two.
- Implementations that stub half the methods with no-op bodies, null/empty placeholders, or unimplemented failures (usually co-occurs with an LSP violation).
- One mock object reconfigured differently across tests because the interface is too broad.
### Common misinterpretation
*"Make interfaces small."* As a count rule, no. ISP is **client-centric** — segregation is driven by *the set of methods a particular client uses*, not by an arbitrary method-count ceiling. Two clients with identical method needs can share one interface even if it has 20 methods.
### Bad
```text
UserService
create(...)
read(...)
update(...)
delete(...)
email(...)
notify(...)
audit(...)
export(...)
```
The audit logger only needs `audit`. It now depends transitively on the email and export subsystems.
### Good
```text
UserAuditor
audit(...)
UserNotifier
notify(...)
```
Implementations can satisfy multiple protocols. Callers depend only on what they use.
---
## D — Dependency Inversion Principle
**Definition (verbatim, two clauses).**
*(a) High-level modules should not depend on low-level modules. Both should depend on abstractions.*
*(b) Abstractions should not depend on details. Details should depend on abstractions.*
Source: Martin's 1996 *C++ Report* paper, archived at Wayback / objectmentor.com.
### Why
Control the direction of the import graph. Policy must not transitively `import` mechanism, or policy becomes un-reusable and untestable.
### Smells to flag
- A high-level module imports a concrete low-level client inside business logic.
- A constructor that `new`/instantiates concrete collaborators instead of accepting them as parameters.
- Abstractions defined in the *low-level* package (the interface lives next to its database or service implementation) — ownership reversed. The interface should live in the **client's** package.
- Function signatures typed against concrete classes instead of interfaces, protocols, or abstract contracts.
### Common misinterpretation
*"DIP means use a DI container."* No. DIP is about **the direction of source-code dependencies**. You can satisfy DIP with plain constructor injection and no framework; you can violate DIP while using Spring.
### Bad
```text
// billing/charge — high-level policy
import SqlUserRepository // concrete import
chargeUser(userId, amount):
repository = new SqlUserRepository() // concrete instantiation
user = repository.get(userId)
...
```
### Good
```text
// billing/user-repository — abstraction lives WITH the client (billing)
UserRepository
get(userId) -> User
// billing/charge
chargeUser(userId, amount, repository: UserRepository):
user = repository.get(userId)
...
// sql/user-repository — detail depends on the abstraction
SqlUserRepository satisfies UserRepository
get(userId) -> User
```
The import arrows go: `sql → billing` (detail → abstraction). They do not go `billing → sql`.
---
## How AI-generated code typically breaks SOLID
Mapped to the principle each breaks:
1. **God-module** from "do everything in one file" prompts — SRP + DIP + usually OCP.
2. **Type-tag dispatch chains** (`if kind == "pdf": ...`) — OCP.
3. **Unsupported-operation stubs in subclasses** when asked to "implement only the methods we need" — LSP + ISP.
4. **Concrete SDK/client imports at module load time** — DIP, hard to test without patching the runtime.
5. **Mega-`Service` interfaces** with create/read/update/delete/email/notify/audit/export — ISP, usually SRP too.
6. **Silent precondition strengthening on override** — defensive-looking, breaks LSP because callers holding the base type now crash on previously-valid inputs.
7. **Invariant-breaking "convenience" subclasses** (e.g., `ReadOnlyList(list)` overriding `append` to no-op) — LSP.
8. **Inverted ownership of abstractions** — putting the interface/protocol/abstract contract in the same file as the concrete implementation. Cosmetic DIP fix, real dependency graph unchanged.
---
## Self-check for SOLID
Before you ship code:
1. (SRP) Does any class in the diff answer to more than one stakeholder group?
2. (OCP) Does any change require a type-tag branch added to an existing function? Could it be data-driven (registry/strategy) instead?
3. (LSP) Does any new subclass signal "not implemented", tighten preconditions, or weaken postconditions?
4. (ISP) Does any interface have a method your concrete client doesn't use?
5. (DIP) Does the high-level package import the low-level concrete? Where do new abstractions live — with the client or with the implementation?