skip to content

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.

level: principalimportance: nice to knowfreq 27%

answer

  1. two audiences: callers and subclasses
  2. self-use pattern (which of my own methods I call) is a contract
  3. Kotlin final unless open -> all-open plugin exists for a reason
  4. Swift: public = use, open = subclass; narrowing is source-breaking
  5. Python: no enforcement; __name mangling blocks overriding on purpose

basics

~20 s

Every 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 s

An 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 lines
kotlin
abstract 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

for a junior

Know that protected members are visible to subclasses and are therefore part of your API, not private implementation.

for a middle

Name one language where classes or methods are closed by default and explain what the author must do to publish a hook.

for a senior

Explain the self-use contract with a concrete break, and give the design rules: final skeleton, overridable hooks, accessors instead of fields.

for a principal

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

context