2.1.15 · D5OOP Fundamentals

Question bank — Composition — has-a relationship vs is-a

1,926 words9 min readBack to topic

Two words you must have straight before starting:

  • Composition = a class holds another object as a field and calls its methods (delegation). "has-a".
  • Inheritance = a class is a kind of another class and must be usable wherever that parent is expected (Liskov). "is-a".

Two pictures to hold in your head

The whole page is a series of traps around which arrow you draw between two boxes. Fix the two arrows first.

Figure — Composition — has-a relationship vs is-a

Look at the figure: in is-a (left) the child box sits inside the parent's territory — it inherits the parent's whole surface, so a change to the parent ripples down. In has-a (right) the two boxes stay separate and only a thin arrow (a method call) connects them. That thin arrow is the entire dependency — that is why composition is looser.

The next figure turns that thin arrow into an actual call graph — who calls whom when car.start() runs:

Figure — Composition — has-a relationship vs is-a

Notice the caller only touches Engine's public method ignite(); it never reaches into the engine's internals. That single narrow contact point is what makes swapping the engine safe. Keep both pictures in mind as you answer below.


True or false — justify

Every answer is one or two sentences of reasoning — never a bare true/false.

"A Car that stores an Engine field is using inheritance."
False — storing another object as a field and delegating to it is composition (has-a); inheritance would be class Car(Engine), which nonsensically claims a car is an engine.
"If two classes share code, one should inherit from the other."
False — shared code is a reuse need, not an is-a claim; you can reuse by holding the helper and delegating, keeping the interfaces independent.
"class Stack(list) is safe because push and pop behave correctly."
False — the danger is the inherited surface: insert, indexing, and slicing are all public, so a user can poke the middle and violate LIFO even though your own methods are fine.
"A part held by composition can always be swapped at runtime, no matter how it was set up."
False — swapping requires the part to be reachable as a settable/injected field (car.engine = ElectricEngine()); a part hard-created and never re-exposed inside the constructor is still composition but cannot be swapped from outside. Composition enables runtime swapping, it does not guarantee it.
"A part that is injected (passed into the constructor) or stored in a public/settable field can be swapped while the program runs."
True — because the part is just an ordinary field with an accessible name, reassigning it (car.engine = ElectricEngine()) takes effect immediately; this is the precise condition under which composition permits runtime swapping.
"Favoring composition over inheritance means never using inheritance."
False — it's a tie-breaker, not a ban; when the is-a claim is genuinely true and substitutable (like Circle(Shape)), inheritance is the correct tool.
"A Person class with a name string field is an example of composition."
False — composition means delegating behavior to a held object that models a real part; a plain data field like name is just state, not a collaborator you call methods on.
"Inheritance couples classes more tightly than composition."
True — the subclass depends on the parent's internals and full interface (which can change), whereas composition depends only on the part's small public method set, a smaller and more stable contract.
"An amphibious robot built from a Flyer part and a Swimmer part needs its own dedicated class."
False — that's the whole point of composition avoiding class explosion; you mix parts at runtime (Robot([Flyer(), Swimmer()])) instead of writing a FlyingSwimmingRobot class.
"If A has-a B, then B is necessarily hidden from A's users."
False — has-a describes the relationship, not visibility; hiding the part (like _items in a Stack) is a separate encapsulation decision you make to protect invariants.
"Inheriting from an abstract base class or protocol (interface inheritance) is just as risky as inheriting from a concrete class."
False — interface inheritance borrows only a contract (method names, no implementation), so there is no parent state or logic to break; the fragile-base-class risk is specific to implementation inheritance where you inherit real code.

Spot the error

State what is wrong and why, in reasoning terms.

The two boxes below are exactly the is-a / has-a arrows from the opening figure — for each "error" ask which arrow was drawn wrong.

"class Password(str) — a password is a string, so inherit."
Wrong — a password inherits every string method (upper, slicing, concatenation, __str__ that leaks it), so its secret is fully exposed and its validation rules can be bypassed; hold a string privately and expose only safe methods.
"class EmailQueue(list) because a queue is a list of emails."
Wrong — it inherits append, insert, sort, and indexing, so FIFO ordering can be broken from outside; the queue has a list internally and should expose only enqueue/dequeue.
"class Car(Engine) so the car can call ignite() directly."
Wrong — this claims a car is an engine, failing substitutability (you can't use a Car where an Engine is required), and it fuses the two lifecycles; the car should hold an engine and delegate start() to engine.ignite().
"To reuse a Logger, I inherit: class Service(Logger)."
Wrong — a service is not a kind of logger, so this violates is-a; it also drags the logger's full interface into the service's public API. Give the service a logger field and call self.logger.log(...).
"I made Stack hold a list but also did class Stack(list) for convenience."
Wrong — you get the worst of both: the hidden list is protected, but the inherited list methods still leak on the Stack itself, so the LIFO guarantee is still breakable.
"class Robot: def __init__(self): self.flyer = Flyer() — abilities are hard-coded, but it's still composition, so it's flexible."
Half-wrong — it is composition, but creating the part internally means it can't be swapped or injected; passing abilities in (dependency injection) is what unlocks the runtime flexibility composition is praised for.
"class PaymentGateway(AbstractGateway) where AbstractGateway is an abstract base with no code, only method signatures — 'this is risky inheritance.'"
Wrong to fear it — inheriting an abstract base with no implementation is interface inheritance: you only promise to provide charge(), refund(), etc., so there is no parent logic to break and this is a legitimate, safe use of inheritance.

Why questions

"Why does the English 'is-a' test actually predict a correct type relationship?"
Because inheritance means substitutability (Liskov): the child must work anywhere the parent is expected, and "A is a B" grammatically asserts exactly that A can stand in for a B.
"Why is a changing parent class dangerous for inherited children but not for held parts?"
A subclass depends on the parent's internals, so an internal change can silently break it (the fragile base class problem); a holder depends only on the part's public methods, a smaller contract that changes far less.
"Why does hiding the internal list fix the broken Stack?"
The bug was never in push/pop — it was in the exposed extra methods; by holding the list as _items and publishing only push/pop/size, the dangerous middle-poking operations simply aren't reachable, so LIFO can't be violated.
"Why does composition avoid the 2^n class explosion that inheritance suffers?"
Inheritance bakes each behavior combination into a distinct compile-time class; composition turns behaviors into part-objects you assemble in a list at runtime, so N behaviors give one class and any mix of parts.
"Why is passing the engine in (rather than building it inside Car) worth the extra parameter?"
Injection lets you supply any engine — petrol, electric, or a mock for tests — without editing Car, decoupling the car from which concrete engine it uses.
"Why is 'a car has an engine' fine but 'a car has an animal' still nonsense?"
has-a only makes sense when the held object is a genuine part the whole delegates work to; an animal isn't a component of a car, so neither is-a nor has-a applies — the relationship itself is invalid.
"Why is interface inheritance often considered compatible with 'favor composition', while implementation inheritance is the thing being warned against?"
Interface inheritance shares only a contract (no code, no state), so it does not create the tight, fragile coupling the guideline targets; the warning is really about inheriting implementation you can accidentally break or become locked into.

Edge cases

"Is delegating to an object you never actually call methods on still composition?"
Not meaningfully — composition is defined by delegating behavior; if you only store the object and never call it, you have a data field, not a has-a collaboration.
"What if a class holds a part AND is genuinely a subtype of something else?"
Both can coexist — e.g. Circle(Shape) (true is-a) could also hold a Renderer (has-a); use inheritance for the substitutable kind-of relationship and composition for the swappable behavior.
"What happens to composition when the held part is None or not yet set?"
Delegation will fail at call time (e.g. self.engine.ignite() raises on None), so a composed class must decide: require the part in the constructor, provide a safe default part, or guard before delegating.
"Can inheritance ever be the safer choice than composition?"
Yes — when the is-a claim is airtight and you want automatic substitutability and interface conformance (like framework base classes or Shape/Circle), inheritance expresses intent more directly than manually re-delegating every method.
"Is a subclass that overrides every parent method still real inheritance, or disguised composition?"
It's a code smell — if nothing of the parent is actually reused and you only wanted the type, consider composition plus an interface; total override often signals the is-a claim was never true.
"Two objects each hold a reference to the same shared part — is that still composition?"
That leans toward aggregation (shared, independent lifetime) rather than strict composition (part owned and destroyed with the whole); the has-a wording still holds, but ownership differs — see Aggregation vs Composition (UML).
"Where does an abstract base class / protocol sit on the composition-vs-inheritance line?"
It is inheritance of interface only — you inherit the promise to implement methods but zero implementation, so it gives you polymorphism and substitutability without importing fragile parent code; it is the safe, contract-shaped end of inheritance.

Connections

  • ← Back to parent topic
  • Inheritance — is-a relationship
  • Liskov Substitution Principle
  • Encapsulation and Information Hiding
  • Dependency Injection
  • Strategy Pattern
  • Aggregation vs Composition (UML)
  • Fragile Base Class Problem