summaryrefslogtreecommitdiff
path: root/prompts/skills/solid-principles/references/lsp.md
blob: bfe74b7af4aae066c9c376b4420009fd1e6331f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# Liskov Substitution Principle (LSP)

> "Objects of a supertype shall be replaceable with objects of a subtype without
> altering the correctness of the program."
> — Barbara Liskov

## Core Idea

If class `B` extends class `A`, you should be able to use `B` anywhere `A` is
expected without surprises. Subtypes must honor the behavioral contract of their
parent — not just the type signature, but the semantic expectations: preconditions,
postconditions, invariants, and side effects.

LSP is the formal foundation of polymorphism. When it's violated, inheritance
becomes a liability rather than a tool.

## Violation Patterns

### 1. Refusing Inherited Behavior
**Heuristic**: A subclass overrides a method to throw `NotImplementedError`,
return `None`, or do nothing — effectively saying "I don't support this."

**Look for**:
- `raise NotImplementedError` in an override.
- Override that returns a hardcoded empty/null value.
- Override with `pass` as the body.
- Comments like "not applicable for this subtype."

**Refactoring**: The inheritance hierarchy is wrong. Either extract the
unsupported method into a separate interface/mixin, or restructure the hierarchy
so the subclass isn't forced to inherit behavior it can't fulfill.

### 2. Strengthened Preconditions
**Heuristic**: A subclass method requires MORE from its inputs than the parent's
contract promises.

**Look for**:
- Added `if` guards at the start of an override that reject inputs the parent
  would accept (e.g., parent accepts any int, subclass rejects negatives).
- Type narrowing in overrides (e.g., parent accepts `Animal`, subclass demands `Dog`).
- Additional validation not present in the parent.

**Refactoring**: Subtypes should accept at least everything the parent accepts.
Widen preconditions or rethink the hierarchy.

### 3. Weakened Postconditions
**Heuristic**: A subclass method provides LESS in its return value than the
parent guarantees.

**Look for**:
- Parent returns a fully populated object; subclass returns a partial/null result.
- Parent guarantees sorted output; subclass doesn't maintain the order.
- Parent guarantees non-null; subclass may return null.

**Refactoring**: Subtypes must deliver at least what the parent promises.
Strengthen the subclass implementation or adjust the contract.

### 4. Violated Invariants
**Heuristic**: A subclass breaks a property that the parent always maintains.

**Look for**:
- Parent maintains `balance >= 0`; subclass allows negative balance.
- Parent ensures a collection is always sorted; subclass inserts without sorting.
- State machine invariants broken (e.g., skipping required transitions).

**Refactoring**: Either enforce the invariant in the subclass or don't inherit.

### 5. The Classic Rectangle-Square Problem
**Heuristic**: A subclass constrains independent properties of the parent, creating
contradictory behavior under mutation.

**Look for**:
- Subclass overrides setters to enforce constraints that break parent behavior
  (e.g., `Square.set_width()` also sets height).
- Tests that pass for the parent but fail for the subclass.

**Refactoring**: Use composition or a common interface instead of inheritance.
Make `Square` and `Rectangle` siblings, not parent-child.

### 6. Exception Surprises
**Heuristic**: A subclass throws exceptions that callers of the parent type
wouldn't expect.

**Look for**:
- Override that throws new checked/unchecked exception types not in the parent's
  contract.
- Override that can fail in scenarios where the parent is documented as safe.

**Refactoring**: Subtypes should only throw exceptions that are subtypes of
the parent's declared exceptions, or handle errors internally.

## Language-Specific Notes

- **Python**: Duck typing makes LSP violations subtle — they manifest at runtime
  as `AttributeError` or unexpected `None`. Watch for Protocol/ABC mismatches.
- **Java/C#**: The compiler enforces type signatures but NOT behavioral contracts.
  Look for `@Override` methods that change semantics.
- **TypeScript**: Structural typing means you can create implicit subtypes that
  violate LSP. Watch for interface implementations that throw on "unsupported"
  methods.
- **Go**: No inheritance, but interface satisfaction can be violated if a concrete
  type's method has different semantics than the interface implies.

## False Positives to Avoid

- **Template Method pattern**: Abstract methods that are *designed* to be overridden
  with different behavior are not LSP violations — the parent's contract explicitly
  delegates to subclasses.
- **Intentional restriction**: If a subclass is documented as a restricted variant
  and callers are aware, this may be acceptable (though it suggests composition
  over inheritance).
- **Different behavior, same contract**: A `LinkedList` and `ArrayList` behave
  differently in performance characteristics but both honor the `List` contract.
  That's fine.