Design Principles¶
Principles help evaluate designs; they are not rules that mechanically produce one correct class structure. Apply them to reduce the cost of likely changes, not to maximize interfaces or layers.
Cohesion, coupling, and information hiding¶
- Cohesion asks whether a module's responsibilities belong together.
- Coupling asks how much one module knows about and depends on another.
- Information hiding keeps volatile design decisions behind stable boundaries.
High cohesion and intentional, narrow coupling generally make local reasoning easier. Zero coupling is neither possible nor desirable: collaborators must share a contract to work together.
SOLID¶
Single Responsibility Principle¶
A module should have one cohesive responsibility or one primary reason to change from the perspective of its stakeholders. “One method per class” is not the principle. Group behavior that changes for the same reason.
Open/Closed Principle¶
Software entities should permit appropriate extension without requiring edits to stable, tested policy. Strategies and composition can provide extension points, but speculative extension points add cost. First identify a credible axis of change.
Liskov Substitution Principle¶
A subtype must honor the behavioral expectations of its supertype. It must not strengthen accepted-input preconditions, weaken promised postconditions, or violate invariants and relevant history constraints.
Interface Segregation Principle¶
Clients should not depend on methods they do not use. Prefer role-focused interfaces to a large interface serving unrelated clients. Do not split a cohesive protocol into fragments that can no longer express its invariants.
Dependency Inversion Principle¶
High-level policy and low-level details should depend on abstractions shaped by the policy. Dependency injection is a construction technique that can support this principle, but the two are not synonyms.
interface BookCatalog {
Optional<Book> find(BookId id);
}
final class FindBook {
private final BookCatalog catalog;
FindBook(BookCatalog catalog) {
this.catalog = catalog;
}
}
The use case defines the capability it needs. A database adapter can implement that capability without making database concepts part of the use-case policy.
Other useful principles¶
- DRY: avoid multiple authoritative representations of the same knowledge; similar-looking code is not necessarily the same knowledge.
- YAGNI: do not implement speculative capability without a present need.
- KISS: prefer the simplest design that satisfies the real constraints.
- Tell, do not ask: place decisions with the object that owns the relevant invariant, while still allowing read models and reporting code.
- Least knowledge: limit navigation through collaborators, but do not hide necessary domain structure behind meaningless forwarding methods.
Package and architecture boundaries¶
Dependency direction matters more than folder names. A boundary earns its cost when it isolates volatility, ownership, policy, deployment, or a testing seam. Adding controller/service/repository layers to every trivial operation is not a proof of good design.
Exercises¶
- Identify which kind of change would affect each class in a small checkout flow.
- Give an example where removing duplicate-looking code better preserves DRY.
- Distinguish dependency inversion from constructor injection.