Classes — Interview Questions¶
50+ questions on class design, SRP, SOLID, cohesion, composition vs inheritance, and the god/data/utility-class smells. Grouped by tier. Each answer is crisp; harder questions flag what the interviewer is really checking. Use as self-review or interview prep.
Table of Contents¶
- Junior — what a class is, SRP, basic smells
- Mid-level — all of SOLID, cohesion, composition vs inheritance
- Senior — LSP, LCOM, DIP, dependency injection, Go's OO
- Staff — architecture-scale SOLID, fitness functions, trade-offs
- Trick Questions
- Rapid-Fire
- Summary
- Further Reading
- Related Topics
Junior (15 questions)¶
Q1. What is a class, conceptually?¶
Answer
A bundle of related data (fields) and the behaviour that operates on it (methods), exposing a small public interface and hiding its internals (encapsulation). A well-designed class models one concept and keeps its data private, exposing operations rather than raw state.Q2. What is the Single Responsibility Principle?¶
Answer
A class should have **one reason to change**. Robert C. Martin's precise definition: *a module should be responsible to one, and only one, actor* — one group of stakeholders whose needs drive changes. If billing rules and report formatting both force edits to the same class, two actors share it, and a change for one risks breaking the other.Q3. What's wrong with the phrase "a class should do one thing"?¶
Answer
It's too vague — "one thing" can be read at any granularity. The actor-based definition is sharper: group methods by *who* requests changes. A `Employee` class with `calculatePay()` (finance), `reportHours()` (operations), and `save()` (DBAs) serves three actors → three responsibilities, even though informally it's "just about employees."Q4. What is a god class?¶
Answer
A class that knows or does too much — thousands of lines, dozens of fields, methods touching unrelated concerns. It centralizes logic that belongs in many classes. Symptoms: everyone edits it, merge conflicts are constant, and you can't test one behaviour without constructing the whole world. Cure: Extract Class along responsibility seams.Q5. What is a data class (as a smell)?¶
Answer
A class with only fields and getters/setters — data with no behaviour. Logic that belongs on the data lives elsewhere (often a god class manipulating the data class's fields). It's the "anemic" half of an anemic domain model. Cure: move the behaviour onto the data (Move Method) so the class owns invariants over its own state. *Note:* a pure data carrier (DTO, `record`, config struct) at a system boundary is fine — the smell is when domain logic that *should* be on the type lives outside it.Q6. What is a utility class?¶
Answer
A class with only static methods and no instance state — `StringUtils`, `MathHelpers`. Convenient, but it can't be mocked, can't be substituted via an interface, and tends to become a dumping ground with no cohesion. Prefer instance methods on the relevant type, or inject a small collaborator you can stub in tests.Q7. Why prefer small classes?¶
Answer
Smaller classes are easier to name, understand, test, and reuse. A class you can describe in one sentence without "and" usually has one responsibility. Large classes hide multiple concepts and force readers to learn everything before changing anything.Q8. What is encapsulation and why does it matter for class design?¶
Answer
Keeping fields private and exposing behaviour, not state. It lets a class enforce its invariants (you can't set a negative balance if there's no public setter) and change internals without breaking callers. A class full of public getters/setters has no encapsulation — it's a data class.Q9. Composition vs inheritance — one sentence each.¶
Answer
**Inheritance** ("is-a"): a subclass *is a kind of* its parent and inherits its interface and implementation. **Composition** ("has-a"): a class holds other objects and delegates to them. Default to composition; reach for inheritance only when there's a genuine is-a relationship *and* substitutability.Q10. What does "favor composition over inheritance" mean?¶
Answer
When you want code reuse, hold a collaborator and delegate rather than subclassing it. Inheritance couples you to the parent's implementation and exposes its whole interface; composition lets you expose only what you need and swap the collaborator. From the GoF book: *favor object composition over class inheritance.*Q11. Name the five SOLID principles.¶
Answer
- **S** — Single Responsibility - **O** — Open/Closed - **L** — Liskov Substitution - **I** — Interface Segregation - **D** — Dependency InversionQ12. What is the Open/Closed Principle?¶
Answer
Software entities should be **open for extension, closed for modification**. You should be able to add behaviour by writing new code (a new subclass/implementation/strategy), not by editing existing, tested code. Achieved via polymorphism: depend on an abstraction, add new implementations.Q13. A Rectangle has setWidth/setHeight. Should Square extend it?¶
Answer
No — the classic LSP violation. A `Square` that keeps width == height breaks `setWidth(5); setHeight(4)` (a caller expects width to stay 5). Inheriting for the "is-a" intuition ("a square is a rectangle") breaks behavioural substitutability. Model them separately, or use an immutable `Shape` with an `area()` method.Q14. Why are deep inheritance hierarchies a smell?¶
Answer
Each level adds coupling: a subclass depends on every ancestor's implementation, and a change near the root ripples down unpredictably. Reading one class means reading the whole chain. Rule of thumb: keep hierarchies 2–3 levels deep; beyond that, prefer composition or strategy objects.Q15. What does it mean to inherit "for code reuse" and why is it risky?¶
Answer
Subclassing solely to grab some methods, with no real is-a relationship — e.g., `Stack extends Vector` (Java's historical mistake) so `Stack` "gets" the list operations. The subclass leaks the parent's whole interface (you can `insertElementAt` into a stack), and you're coupled to the parent's implementation. Reuse via composition instead: hold a `List` and delegate.Mid-level (15 questions)¶
Q16. Give a clean SRP refactor of a class that violates it.¶
Answer
// Before: three actors share one class
class Employee {
Money calculatePay() { ... } // finance
String reportHours() { ... } // operations
void save() { ... } // DBAs / persistence
}
// After: one responsibility each
class PayCalculator { Money calculate(Employee e) { ... } }
class HoursReporter { String report(Employee e) { ... } }
class EmployeeRepository { void save(Employee e) { ... } }
Q17. Show Open/Closed with and without polymorphism.¶
Answer
// Violates OCP: adding a shape edits this method
double area(Shape s) {
if (s instanceof Circle c) return Math.PI * c.r * c.r;
if (s instanceof Square sq) return sq.side * sq.side;
// adding Triangle → edit here
}
// Honors OCP: add a shape by adding a class
interface Shape { double area(); }
record Circle(double r) implements Shape { public double area(){ return Math.PI*r*r; } }
record Square(double side) implements Shape { public double area(){ return side*side; } }
// Triangle: new class, zero edits to existing code
Q18. State the Liskov Substitution Principle precisely.¶
Answer
If `S` is a subtype of `T`, objects of type `T` may be replaced with objects of type `S` **without altering the correctness** of the program. Concretely, a subtype must not strengthen preconditions, weaken postconditions, or violate invariants of the supertype. "It compiles" is not enough — behaviour must remain substitutable.Q19. What is the Interface Segregation Principle?¶
Answer
Clients should not be forced to depend on methods they don't use. Prefer many small, role-specific interfaces over one fat interface. A `Machine` interface with `print()`, `scan()`, `fax()` forces a simple printer to implement (or stub-throw) `scan` and `fax`. Split into `Printer`, `Scanner`, `Fax`.Q20. Show an ISP violation and its fix.¶
Answer
// Fat interface — old printer must stub scan/fax
interface MultiFunction { void print(Doc d); void scan(); void fax(); }
// Segregated roles — implement only what you support
interface Printer { void print(Doc d); }
interface Scanner { void scan(); }
class SimplePrinter implements Printer { public void print(Doc d){...} }
class OfficeMachine implements Printer, Scanner { ... }
Q21. What is cohesion?¶
Answer
How focused a class's responsibilities are — how much its methods and fields belong together. High cohesion: every method uses most of the fields; the class is "about" one thing. Low cohesion: disjoint clusters of methods touching disjoint fields — a sign two classes are hiding inside one.Q22. What is LCOM and what does a high value mean?¶
Answer
**Lack of Cohesion of Methods** — a metric for how *un*cohesive a class is. Intuition: pairs of methods that share no fields are "disconnected." High LCOM means methods form separate islands touching separate fields → the class wants to be split. LCOM4 (the common variant) counts connected components of the method/field graph; > 1 component literally means the class is two or more classes glued together.Q23. What is coupling, and how does it relate to cohesion?¶
Answer
Coupling is how much one class depends on the internals of another. The design goal is **low coupling, high cohesion**: classes that are internally focused (cohesive) and externally independent (loosely coupled). Inheritance is high coupling (you depend on the parent's implementation); depending on a narrow interface is low coupling.Q24. What is Dependency Inversion?¶
Answer
Two rules: (1) high-level modules should not depend on low-level modules — both depend on **abstractions**; (2) abstractions should not depend on details — details depend on abstractions. So an `OrderService` depends on a `PaymentGateway` *interface*, not on `StripeClient` directly. The arrow of dependency is "inverted" to point at the abstraction.Q25. What is dependency injection, and how does it differ from DIP?¶
Answer
DIP is the *principle* (depend on abstractions). DI is a *technique* for satisfying it: pass a class's dependencies in (via constructor, setter, or method) rather than having it `new` them itself. Constructor injection is preferred — dependencies are explicit, final, and the object is always fully constructed.Q26. Constructor vs setter vs field injection — which and why?¶
Answer
**Constructor injection** by default: required dependencies are explicit, the object is immutable and never half-built, and tests can pass fakes directly. **Setter injection** only for genuinely optional dependencies. **Field injection** (reflection-based) hides dependencies, breaks immutability, and can't be used without the container — avoid it.Q27. When is inheritance the right choice?¶
Answer
When all hold: (1) a true **is-a** relationship, (2) the subtype is **behaviourally substitutable** (LSP), (3) you intend to inherit the parent's *interface*, not just steal its code, and (4) the hierarchy stays shallow. Template Method and stable framework base classes are good fits. If you're only after reuse, compose.Q28. How does composition enable swapping behaviour at runtime?¶
Answer
A composed collaborator can be replaced after construction; an inherited base class is fixed at compile time. This is the Strategy pattern: `new Sorter(new QuickSort())` vs `new Sorter(new MergeSort())`. With inheritance you'd need a separate subclass for each combination, causing class explosion.Q29. What's the relationship between SRP and the Single Responsibility of a function?¶
Answer
Same idea at different scope. A function should do one thing; a class should have one reason to change. SRP at the class level is often achieved by extracting cohesive functions first, then noticing which functions cluster around which fields, then extracting those clusters into their own classes.Q30. How do you detect a god class in review?¶
Answer
Signals: very high line/method/field count; the class name is vague (`Manager`, `Processor`, `Helper`, `Util`); many imports from unrelated subsystems; high LCOM; it appears in most PR diffs (high change frequency). Confirm by trying to write a one-sentence description — if you need "and," it's doing too much.Senior (13 questions)¶
Q31. What the interviewer is checking: Walk me through identifying actors to apply SRP on a real class.¶
Answer
*They want to see if you operationalize SRP rather than recite it.* For each public method, ask "who asks for this to change, and why?" Group methods by that answer. The groups are your candidate classes. Then check field usage: methods in a group should share fields (high cohesion); if a group touches fields no other group uses, that's a clean Extract Class seam. Validate with LCOM if you have tooling.Q32. How would you measure cohesion across a codebase, and what would you do with the numbers?¶
Answer
Run an LCOM metric (LCOM4 in tools like Sonar, `lcom` plugins, or `jdepend`). Treat it as a *triage signal*, not a gate — sort classes by LCOM × change-frequency to find cohesion hotspots worth refactoring. A high-LCOM class that never changes is low priority; a high-LCOM class everyone edits is where bugs cluster. Don't fail builds on raw LCOM — it has false positives (e.g., classes with legitimately independent helper methods).Q33. Give a subtle LSP violation that compiles fine.¶
Answer
`ImmutableList implements List` but throws `UnsupportedOperationException` on `add()`. It satisfies the type but violates the contract — callers holding a `List` expect `add` to work. The subtype *strengthened a precondition* (you may only call mutators if mutable). Fix: a narrower interface (`ImmutableCollection` without mutators) so substitutability holds for what the type actually promises.Q34. How does DIP enable testing and what does the test look like?¶
Answer
Because the class depends on an interface, tests inject a fake. No network, no Stripe, no container. The seam created by DIP *is* the test seam.Q35. How does Go do object-orientation without classes or inheritance?¶
Answer
- **No classes:** methods are attached to types via receivers (`func (s *Server) Start()`). - **No inheritance:** reuse is via **struct embedding** (composition that promotes the embedded type's methods). - **Interfaces are satisfied implicitly:** a type implements an interface just by having the methods — no `implements` keyword. This makes ISP and DIP natural: define small interfaces *at the consumer*, and any type with the right methods fits. So Go bakes "favor composition" and "depend on abstractions" into the language by simply not offering inheritance.Q36. Show Go's implicit interfaces satisfying DIP and ISP.¶
Answer
// Consumer declares the narrow interface it needs (ISP)
type Notifier interface { Notify(msg string) error }
type OrderService struct { n Notifier } // depends on abstraction (DIP)
// Any type with Notify() satisfies it — no "implements"
type EmailSender struct{}
func (EmailSender) Notify(msg string) error { /* ... */ return nil }
Q37. Is struct embedding the same as inheritance?¶
Answer
No. Embedding promotes the embedded type's methods (syntactic convenience) but there's **no subtype polymorphism** — an `*Server` embedding `Logger` is not a `Logger` subtype, and there's no virtual dispatch / method overriding through the embedded field. It's composition with delegation sugar. You get reuse without the substitutability obligations (and pitfalls) of inheritance.Q38. How do you refactor a deep inheritance hierarchy?¶
Answer
Replace inheritance with delegation incrementally: (1) identify the varying behaviour the subclasses provide; (2) extract it into a Strategy interface; (3) have the base class hold a strategy field instead of relying on overridden methods; (4) convert each subclass into a strategy implementation; (5) collapse the now-empty subclasses. This flattens N levels into one class + a set of pluggable strategies (Replace Inheritance with Delegation).Q39. What the interviewer is checking: When does applying SOLID make the design worse?¶
Answer
*They want judgment, not dogma.* Over-applied SOLID produces interface-per-class, indirection everywhere, and "Enterprise FizzBuzz." Specific failure modes: SRP taken to one-method classes (shotgun surgery to make any change); OCP abstractions added for axes of change that never materialize (YAGNI); DIP creating interfaces with exactly one implementation forever. SOLID is a set of *forces to balance*, applied when you've seen the change actually happen — not a checklist to maximize.Q40. How do you decide between a value object and a service class?¶
Answer
A **value object** is defined by its attributes, immutable, equality-by-value, and owns behaviour over its own data (`Money.plus(Money)`). A **service** is stateless, holds collaborators, and orchestrates across objects (`TransferService`). If logic belongs to one piece of data, put it on the value object; if it spans multiple objects or needs injected infrastructure, it's a service. Misplacing service logic onto data, or data logic into services, produces the anemic-model smell.Q41. What's wrong with a static utility class from a testing standpoint?¶
Answer
Static calls are hard dependencies — `Utils.now()` or `FileUtils.read(path)` can't be substituted without bytecode-rewriting mock frameworks. Code calling statics isn't isolatable in unit tests. The clean alternative: an injectable instance behind an interface (`Clock`, `FileStore`), so tests pass a fake. Reserve true statics for pure functions with no I/O and no time/randomness dependency.Q42. How does the Dependency Inversion Principle relate to package/module structure?¶
Answer
The abstraction (interface) should live in (or near) the *high-level* module that uses it, and the low-level implementation should depend inward on it — this is "the dependency arrow points at the policy." It prevents the core domain from depending on infrastructure. In Clean/Hexagonal architecture, domain defines ports (interfaces); adapters (DB, HTTP) implement them. The build dependency goes adapter → domain, never the reverse.Q43. Composition has a downside — name it and how you mitigate.¶
Answer
Delegation boilerplate: a composing class must forward many methods to its collaborator, which is verbose and can drift. Mitigations: forward only the narrow interface you actually expose (often a feature, not a cost); use language support (Kotlin `by` delegation, Go embedding, Lombok `@Delegate`); or accept that the explicit forwarding documents exactly what's exposed — unlike inheritance, which leaks everything.Staff (9 questions)¶
Q44. How do Bloater/SOLID violations manifest at the service/architecture level?¶
Answer
God class → god service (a "monolith microservice" owning many bounded contexts). SRP violation → a service that deploys for many unrelated reasons, so every team blocks on it. DIP violation → services importing each other's concrete clients instead of versioned contracts. The cures scale too: bounded contexts (DDD) are Extract Class at architecture scale; contract/interface boundaries are DIP between services.Q45. How would you enforce class-design rules in CI?¶
Answer
Fitness functions. With ArchUnit (Java):@ArchTest static final ArchRule services_are_small =
classes().that().resideInAPackage("..service..")
.should().haveLessThanOrEqualTo(15).methodsThatAreNotPrivate();
@ArchTest static final ArchRule domain_independent_of_infra =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..infra..");
Q46. How do you introduce DIP into a legacy codebase that news everything?¶
Answer
Branch by Abstraction: (1) extract an interface from the concrete dependency; (2) make callers depend on the interface, still wired to the same concrete class; (3) introduce a seam (constructor param or factory) so the implementation can be swapped; (4) now you can inject fakes for tests or alternate implementations. Each step is behaviour-preserving and shippable — no big-bang rewire.Q47. What the interviewer is checking: SRP says "one reason to change" — but isn't every class changed for many reasons in practice?¶
Answer
*They're probing whether you understand "reason" = "actor," not "any edit."* SRP isn't "this class never changes for two literal reasons" — it's "the *responsibilities* this class owns answer to one stakeholder group." A `User` class legitimately changes when the user concept changes. It violates SRP when changes are driven by *different actors* (security policy vs reporting vs persistence schema) who shouldn't be coupled. The test is coupling of *change drivers*, not count of commits.Q48. When is a god class acceptable, at least temporarily?¶
Answer
When it's stable and rarely changed (the cost of touching it is the problem; if you don't touch it, the risk is dormant), or when splitting it requires a test harness you don't yet have. Prioritize refactoring by *churn × size*, not size alone. A 5,000-line class untouched for three years is lower priority than an 800-line class edited weekly.Q49. How do you balance ISP against interface explosion?¶
Answer
Segregate by **client role**, not by individual method. If three methods are always used together by the same client, they belong in one interface. The unit of segregation is "a coherent set of operations one kind of consumer needs." In Go, let consumers define the interface (so it's exactly as wide as that consumer needs); in Java/C#, group by usage observed across call sites, and split a fat interface only when a real client is forced to depend on what it doesn't use.Q50. How does Liskov substitutability interact with covariance/contravariance?¶
Answer
LSP formalizes to: a subtype may have **contravariant** (wider) parameter types and **covariant** (narrower) return types, weaker preconditions, and stronger postconditions/invariants. Most languages restrict method overriding to invariant parameters and covariant returns precisely to stay LSP-safe. Violations show up when an override demands *more* of its input (stronger precondition) or promises *less* about its output than the base contract.Q51. How do DIP and the testing pyramid relate?¶
Answer
DIP is what makes the wide base of fast unit tests possible. Each injected abstraction is a seam where you swap a fake, so the unit under test runs without DB/network/clock. Without DIP you're forced up the pyramid into slow integration tests just to isolate logic. So "design for testability" and "follow DIP" are largely the same instruction — over-mocking, however, signals you've inverted dependencies you should have just called directly (see mocking trade-offs).Q52. Decompose a god class — what's your end-to-end process?¶
Answer
1. **Characterization tests** to pin current behaviour (no specs to trust). 2. **Map responsibilities to actors**; cluster methods + fields. 3. **Identify the highest-churn cluster** — refactor that first for ROI. 4. **Extract Class** along a field/method cluster; original becomes a coordinator. 5. **Invert dependencies** (DIP) where the extracted class calls back into infrastructure. 6. **Repeat**, one cluster per PR, tests green throughout. Never one big-bang split — that's a rewrite, and rewrites re-introduce solved bugs.Trick Questions (8)¶
T1. Is inheritance always bad?¶
Answer
**No.** It's a powerful tool when the relationship is genuinely is-a *and* substitutable: Template Method, sealed type hierarchies modeling a closed set of variants, stable framework base classes. The advice is "favor composition," not "ban inheritance." Inheritance is bad when used purely for code reuse, when it breaks LSP, or when it grows deep.T2. Is a class with only static methods OK?¶
Answer
**Sometimes.** For genuinely pure functions (`Math.max`, string formatting with no I/O), a static utility is fine and even clearer than forcing an instance. It becomes a smell when it (a) hides I/O or time/randomness you'd want to fake in tests, (b) holds mutable static state, or (c) becomes a low-cohesion grab-bag. Rule: static for pure functions; instance behind an interface for anything with side effects or substitutability needs.T3. Is SOLID always right?¶
Answer
**No — they're heuristics, not laws.** Each principle trades simplicity for flexibility. Applied prophylactically (before you know the axis of change), they add indirection that never pays off (YAGNI). SOLID earns its keep where change has actually been observed. A small script, a one-implementation interface, or a stable value object often shouldn't be "SOLID-ified." Judgment > dogma.T4. How small is too small for a class?¶
Answer
When splitting forces **shotgun surgery** — a single logical change now requires editing many tiny classes in lockstep. That's low cohesion expressed as fragmentation: the pieces only make sense together. If two classes always change together and never independently, they're one concept artificially split. Cohesion is the test in *both* directions: too big (multiple reasons to change) and too small (pieces that can't change independently) are both failures.T5. A data class is always a smell — true?¶
Answer
**False.** A pure data carrier is correct at boundaries: DTOs, API request/response bodies, config structs, `record`/`@dataclass` value holders. The *smell* is when domain logic that should live on the type is scattered elsewhere, leaving the data class anemic *within the domain layer*. Context decides: data-only is right for transport, wrong for a domain entity that should own its invariants.T6. If you depend on an interface with only one implementation, is that pointless DIP?¶
Answer
**Often, yes — but not always.** The two legitimate reasons for a one-impl interface: (1) a **test seam** (the second "implementation" is your fake), and (2) **dependency direction** (the interface lets the high-level module avoid importing the low-level one). If neither applies — no tests use a fake and there's no layering benefit — the interface is speculative and adds noise. Don't add interfaces reflexively.T7. "Has-a" can always replace "is-a" — so why keep inheritance at all?¶
Answer
Because inheritance gives you **subtype polymorphism** for free — callers treat all subtypes uniformly through the base type. Composition can model the data relationship but you'd hand-roll an interface and delegation to get the polymorphism. When you genuinely need "treat these uniformly *and* they're substitutable," a shallow inheritance (or interface + composition) is the cleaner expression. The point isn't that inheritance is irreplaceable, it's that replacing it isn't free.T8. Does Go's lack of inheritance make it less object-oriented?¶
Answer
**No — arguably more disciplined OO.** OO's core ideas are encapsulation, polymorphism, and message-passing; inheritance is just *one* mechanism for reuse and polymorphism. Go keeps encapsulation (package-level visibility), polymorphism (interfaces), and composition (embedding), and drops the one feature most often misused. It's a deliberate design choice that pushes you toward "favor composition" and "depend on small interfaces" by default.Rapid-Fire¶
| Question | Answer |
|---|---|
| SRP in one phrase | One reason to change = one actor |
| Open/Closed | Extend by adding code, not editing it |
| Liskov | Subtypes must be behaviourally substitutable |
| Interface Segregation | Many small role interfaces, not one fat one |
| Dependency Inversion | Depend on abstractions, not concretions |
| DI vs DIP | DI is the technique; DIP is the principle |
| Preferred injection | Constructor |
| Default reuse mechanism | Composition |
| When inheritance is right | True is-a + LSP + shallow |
| God class cure | Extract Class along actor seams |
| Data class cure | Move behaviour onto the data |
| Utility class risk | Can't mock/substitute statics |
| Cohesion metric | LCOM (high = split it) |
| Design goal | Low coupling, high cohesion |
| Max inheritance depth | 2–3 levels |
| Go reuse | Struct embedding (composition) |
| Go polymorphism | Implicit interfaces |
| Is inheritance always bad? | No — misused-for-reuse is |
| Is SOLID always right? | No — heuristics, balance them |
| Too small? | When it causes shotgun surgery |
Summary¶
Good class design reduces to two forces in tension: high cohesion (each class owns one responsibility, measured loosely by LCOM and precisely by the actor test for SRP) and low coupling (classes depend on narrow abstractions, achieved by DIP + dependency injection and by favoring composition over inheritance).
SOLID names five facets of this: SRP (one actor), OCP (extend, don't edit), LSP (substitutability is behavioural, not just type-level), ISP (interfaces sized to client roles), DIP (depend inward on abstractions). They're heuristics — apply them where change has actually been observed, not prophylactically.
The recurring smells are the inverse: the god class (no cohesion, many actors), the anemic data class (data divorced from behaviour), the static utility class (can't substitute), inheritance-for-reuse and deep hierarchies (coupling and broken substitutability). The cures — Extract Class, Move Method, Replace Inheritance with Delegation, introduce-an-interface — are all moves toward higher cohesion and lower coupling. Inheritance isn't banned; it's reserved for true, shallow, LSP-safe is-a relationships. Everything else composes.
Further Reading¶
- Robert C. Martin, Clean Code — Chapter 10 (Classes) and the SOLID essays.
- Robert C. Martin, Clean Architecture — SRP-as-actor and DIP at module scale.
- Gamma et al., Design Patterns — "favor object composition over class inheritance."
- Barbara Liskov & Jeannette Wing, A Behavioral Notion of Subtyping — the formal LSP.
- Martin Fowler, Refactoring — Extract Class, Replace Inheritance with Delegation, anemic domain model.
Related Topics¶
- Classes — README · Junior · Professional
- Chapter README
- Sibling chapter: Objects and Data Structures
- Cross-section: Design Patterns · Refactoring
In this topic