An abstract class exposes shared state and protected hook methods to its subclasses. Explain why that creates a second API you must maintain, and compare how Kotlin's final-by-default classes, Swift's `open` versus `public`, C++'s non-virtual default and Python's convention-only access change what you can safely change later.
answer
- two audiences: callers and subclasses
- self-use pattern (which of my own methods I call) is a contract
- Kotlin final unless open -> all-open plugin exists for a reason
- Swift: public = use, open = subclass; narrowing is source-breaking
- Python: no enforcement; __name mangling blocks overriding on purpose
basics
~20 sEvery protected field, hook and self-call is a contract with subclasses you cannot see, and changing it breaks them like a public change would. Languages differ in the default: Kotlin classes and members are final unless open, Swift types are subclassable outside their module only if open, C++ methods are non-virtual unless virtual, Java and Python are open by default and Python enforces nothing.
solid answer
~50 sAn abstract class has two audiences — callers and subclasses — and the second one also sees protected state, hook order, and which of your own methods you call internally. That self-use pattern is observable: change `render()` to stop calling `header()` and a subclass that overrode `header()` silently stops working. The defaults decide how much of that surface you published by accident. - **Java, Python**: subclassable and overridable by default; you opt out with `final`, and in Python you cannot opt out at all — `_x` is convention and `__x` name-mangling actively breaks a subclass that tries to override it. - **C++, C#**: methods are non-virtual unless declared `virtual`, so the dispatchable surface is opt-in — the reverse default. - **Kotlin**: classes and members are final unless `open`, which is why mocking frameworks need the all-open compiler plugin. - **Swift**: `public` permits use but not subclassing outside the module; `open` permits both, and narrowing `open` to `public` is a source-breaking change.
code
kotlin · 7 linesabstract class Job {
fun run() { step(); onDone() } // skeleton: final, cannot be replaced
protected abstract fun step() // hook: abstract implies open
protected open fun onDone() {} // hook: must say `open`
protected fun onFail() {} // visible to subclasses, NOT overridable
}go deeper
Know that protected members are visible to subclasses and are therefore part of your API, not private implementation.
Name one language where classes or methods are closed by default and explain what the author must do to publish a hook.
Explain the self-use contract with a concrete break, and give the design rules: final skeleton, overridable hooks, accessors instead of fields.
Frame it as an evolution commitment — decide how many independent implementations you will support and for how long, and prefer defaults or explicit sealing so the obligation is opt-in rather than accidental.
## Two audiences, one class A public method has one audience: callers. An abstract class has two. Callers see the public methods. Subclasses see, in addition: every protected field, every protected or overridable method, the order in which the base invokes its hooks, and — crucially — which of its own methods the base calls internally. That last item is called the self-use pattern, and it is the part people forget is a contract. Concretely: a base `Report.render()` that calls `this.header()` and `this.body()` has told subclasses that overriding `header()` changes the rendering. If a later refactor inlines the header logic into `render()`, nothing in the public API changed, no compiler complains, and every subclass that overrode `header()` silently stops affecting the output. The only way to avoid that class of break is to document self-use explicitly — "render() calls header() once, before body()" — which means publishing your implementation, which is the cost of having a subclass API at all. Protected mutable state is the same problem with sharper edges. A protected field is a field with no invariant enforcement: any subclass can assign it, including from a constructor, including on another thread. You can never later make it computed, lazily loaded, or validated without breaking subclasses that wrote to it directly. The mitigation is uniform across languages: expose protected *accessors* rather than protected fields, so the base retains the ability to change representation. ## What the language defaults decide **Java and C++ historically diverge on virtuality.** In Java every non-final, non-private instance method is overridable, so the dispatchable surface is whatever you did not think to mark `final`. C++ (and C#) require `virtual` at the declaration; if you never write it, no subclass can change behaviour through a base-typed reference. The reverse defaults produce opposite failure modes: Java classes leak extension points, C++ classes require you to have anticipated them. **Kotlin flipped Java's default deliberately.** Classes and members are final unless marked `open`, so an abstract class author must publish each hook by name. Two visible consequences: the subclass API becomes a list you can read off the declarations, and mocking or proxy-based frameworks that subclass at runtime stopped working, which is why the ecosystem ships the all-open compiler plugin (and the Spring variant) to reopen classes for tests and proxies. That plugin is worth mentioning because it demonstrates the tradeoff has a real price, not just a benefit. **Swift separates "usable" from "subclassable".** A `public` class can be used across module boundaries but not subclassed there; `open` allows both, and the same distinction applies to individual members. This gives a library author a third state that Java lacks: extensible inside the module, closed outside. It also creates an evolution rule — going from `open` to `public` removes a capability clients may depend on, so it is a source-breaking change and must wait for a major version. **Python enforces nothing, and its one enforcement mechanism cuts the wrong way.** There is no protected or private; `_name` is a convention meaning "do not touch" and `__name` triggers name mangling to `_Class__name`. Mangling exists to prevent accidental collisions in subclasses, and its side effect is that a subclass *cannot* override a mangled method as a hook — a base method calling `self.__step()` is deliberately not extensible. Meanwhile `abc.ABCMeta` prevents instantiation while abstract methods remain, checked at instantiation time rather than at definition time, so a missing hook surfaces when someone constructs the object. **C#'s access modifiers scope the subclass audience.** `protected internal` (this assembly or any subclass) and `private protected` (subclasses in this assembly only) let an author expose hooks to their own extensions without publishing them to the world. That is the fine-grained version of Swift's `open`/`public` split. ## Designing the subclass surface deliberately A workable checklist, language-independent: 1. **Decide whether subclassing is part of the product.** If it is not, seal the class. Kotlin and Swift make this the default or a one-word choice; in Java and Python it is a decision you must make explicitly and defend. 2. **Make the skeleton non-overridable and the hooks overridable.** A `final`/non-virtual template method that calls abstract hooks means the algorithm's shape is yours and only the steps are theirs. 3. **Publish protected accessors, not protected fields**, so representation stays changeable. 4. **Document self-use** for anything overridable, because subclasses will depend on it whether you documented it or not. 5. **Test through the subclass API**, not only the public one — a base class with hooks needs a test subclass that overrides each hook, or the contract is unverified. ## The principal-level framing The real question is not "abstract class or not" but "how many independently evolving implementations do I intend to support, and for how long?" Every hook is a compatibility obligation, and unlike a public method it obliges you to keep an *implementation detail* stable. Teams that discover this late end up with base classes that cannot be refactored because nobody knows which of a hundred subclasses depends on which call order. The languages that default to closed are not being pedantic; they are making the obligation opt-in.
- What is the self-use pattern and why must it be documented?It is the set of calls a base class makes to its own overridable methods — for example a render method that calls a header hook before a body hook. Subclasses depend on those calls to take effect, so removing or reordering them breaks subclasses without changing any public signature. Documenting the self-use turns an implementation detail into a stated contract, which is the only way the base class can later be refactored safely.
- Why do Kotlin projects so often add the all-open compiler plugin, and what does that tell you about final-by-default?Because mocking, proxying and some persistence frameworks work by generating a subclass at runtime, which final classes forbid. The plugin reopens selected classes (often those with framework annotations) so those tools keep working. It shows the tradeoff is real: closing by default removes accidental extension points and also removes the extension points tooling silently relied on.
- Swift lets a library declare a class `public` but not `open`. Why is going from `open` back to `public` a breaking change?Because `open` grants clients outside the module the right to subclass and override; removing it takes that right away, and any client subclass stops compiling. It is the same category of break as removing a public method, even though no signature changed — which is why the capability should be granted deliberately and only where extension is part of the product.
saying these in an interview costs you the question
- Treating protected members as private, forgetting that subclasses are an audience with an evolution contract
- Believing a Python underscore or double underscore prefix enforces access
- Assuming every base class method is overridable, when C++, C# and Kotlin default to non-overridable
- Leaving the template method itself overridable, so a subclass can replace the algorithm it was meant to fill in
- Exposing protected mutable fields instead of accessors and later being unable to change the representation