How does polymorphic creation via Factory Method support the Open/Closed Principle and the Dependency Inversion Principle?
answer
- dynamic dispatch replaces the conditional
- policy declares the abstraction, detail implements it
- new variant = new file, not an edit
- decision concentrates at the composition root
- OCP is relative to one axis of change
basics
~20 sBusiness logic calls an overridable creation method and uses only the product interface, so it never names a concrete class. New behaviour arrives as a new subtype (extension, no edits), and the logic depends on abstractions rather than on concrete implementations.
solid answer
~60 s**Open/Closed** says a module should be open to extension but closed to modification. A constructor call hard-codes a class, so supporting a new variant means editing working code. With Factory Method, the creator's algorithm calls `createProduct()` and manipulates only the abstract product; a new variant is a new concrete creator plus a new product, and no existing, tested file changes. The conditional that would have grown is replaced by dynamic dispatch. **Dependency Inversion** says high-level policy should depend on abstractions, not on low-level details, and that abstractions shouldn't depend on details. `new ConcreteProduct()` inside policy is a source-level dependency from high level to low level. Routing creation through an overridable method flips it: policy declares the abstraction it needs, and the detail (the concrete creator/product) plugs in from outside at wiring time — the dependency arrow now points from detail to abstraction. Caveat: the choice doesn't vanish, it relocates to the composition root. Factory Method buys OCP for the *product axis* only; a change to the product *interface* still ripples everywhere.
go deeper
Explain that logic uses the interface and never says new Concrete, so adding a type means adding a class rather than editing existing code.
Name OCP and DIP explicitly, show dynamic dispatch replacing the conditional, and note who owns the abstraction.
Qualify the benefit: OCP holds only for the product axis; interface changes still ripple; mention Liskov substitutability and testability, and relate the pattern to DI and the composition root.
Discuss choosing variation points deliberately — cost of speculative abstraction, dependency direction across module/package boundaries, and how the composition root keeps concrete-type knowledge in one low-risk place.
## The two principles, defined - **Open/Closed Principle (OCP)** — software entities should be *open for extension, closed for modification*: you can add new behaviour without editing existing source. In practice this means new behaviour arrives as new code (a new type, a new registration) rather than as edits inside code that is already working and tested. - **Dependency Inversion Principle (DIP)** — high-level modules (policy: what the system does) must not depend on low-level modules (mechanism: how it's done); both should depend on abstractions. Additionally, abstractions must not depend on details. ## Why a constructor call breaks both `val t = Truck()` inside `planDelivery()`: 1. **Breaks DIP** — the high-level delivery policy now has a *source-level, compile-time* dependency on the low-level `Truck` class. Change `Truck`'s constructor signature and the policy file must recompile and possibly change. The dependency arrow points policy → detail, the wrong way. 2. **Breaks OCP** — supporting ships means editing `planDelivery()` (adding a conditional) or copy-pasting it. Every new variant reopens the same file, so its risk of regression grows monotonically. The file becomes a *change magnet*. ## How Factory Method fixes it Replace the constructor call with a call to an overridable method that returns the abstract type: ``` abstract class Logistics { abstract fun createTransport(): Transport // abstraction, declared by policy fun planDelivery(o: Order) { // policy, closed for modification val t = createTransport() t.load(o); t.deliver() } } ``` Now: - **DIP**: `Logistics` (high level) depends only on `Transport` (abstraction). Note *who owns the abstraction*: `Transport` is defined by/for the policy's needs, and `Truck` implements it. That is the "inversion" — the detail depends on the abstraction the policy declares, not the reverse. In package terms, the interface belongs on the policy side of the boundary. - **OCP**: adding `AirLogistics` + `Plane` adds files; `planDelivery()` is never reopened. The variation point is the virtual call, and **dynamic dispatch replaces the conditional** — this is the general mechanism by which polymorphism buys OCP. ## The precise, honest scope of the benefit OCP is always *relative to an anticipated axis of change*. Factory Method closes the module against **"a new kind of product"**. It does **not** close it against: - **Changing the product interface** (adding `Transport.trackingId()` forces every implementation to change). Interfaces are the new coupling; keep them narrow — this is where the Interface Segregation Principle helps. - **Changing the algorithm** in the creator (that's Template Method / Strategy territory). - **New construction parameters** — if one product needs data the abstract creator has no access to, the signature leaks and the abstraction erodes. Misjudging the axis is the classic failure: you pay for a hierarchy that varies the wrong thing. ## Where the decision goes: the composition root Something must still pick `RoadLogistics` vs. `SeaLogistics`. That knowledge concentrates at the **composition root** — the single place near program startup where the object graph is assembled (main, a DI container's configuration, a plugin loader). Concentrating it there is the point: exactly one location knows concrete types, and it contains no business logic, so changes there are low risk. Dependency Injection is the mechanized version of the same idea; Factory Method is the hand-rolled, inheritance-based one. ## Related consequences - **Testability**: a test creator returning a fake `Transport` needs no mocking framework and no changes to policy code. Hard-to-test code is usually code that constructs its own collaborators. - **Liskov substitution matters**: OCP only holds if every concrete product is genuinely substitutable. If callers must sniff the concrete type (`if (t is Ship) …`), you have re-introduced the conditional you removed and lost the benefit. - **Cost check**: each variation point adds indirection. If there will only ever be one product, applying the pattern is speculative generality — carrying complexity for a change that never comes. OCP is a tool to apply where change is *demonstrated or strongly expected*, not everywhere.
- Does Factory Method make a module fully closed to modification?No. It closes it against new *kinds of product* only. Changing the product interface, the creation parameters, or the creator's algorithm still requires edits — OCP is always relative to a chosen axis of change.
- How is this related to Dependency Injection?They solve the same coupling with different mechanics. Factory Method varies creation through inheritance and an overridable hook; DI passes the collaborator (or a factory object) in from outside. DI usually scales better because it avoids the parallel class hierarchy, but both concentrate concrete-type knowledge at the composition root.
saying these in an interview costs you the question
- Claiming the pattern eliminates the choice of concrete class rather than relocating it to the composition root.
- Saying it makes code "closed to all modification" — interface changes still ripple to every implementation.
- Applying it where only one implementation exists or is plausible (speculative generality).
- Keeping `if (product is ConcreteX)` checks in the caller, which reinstates the conditional the pattern removed.
- Confusing DIP with "use a DI framework"; DIP is about the direction of source dependencies, not tooling.