skip to content

Abstract Class vs Interface

Shared state or a fixed skeleton points to a partial base; a capability unrelated types can offer points to a contract. One of the most common interview questions in the object model.

on this pageshow

questions

2

In several widely used languages the question 'should this be an abstract class or an interface?' cannot be asked in that form at all. Name such languages, say what replaces the choice there, and explain what actually drives the decision instead.

level: middleimportance: should knowfreq 40%

answer

  1. the choice is an artefact of nominal + single inheritance
  2. Go: no base class, consumer-declared structural interfaces
  3. Rust: traits with defaults, no impl inheritance, orphan rule
  4. C++: interface = abstract class, all pure virtual
  5. Python: abc.ABC (nominal) vs typing.Protocol (structural)

basics

~20 s

Go and Rust have no implementation inheritance, so there is no abstract class: you declare an interface or trait and reuse code by composition or default bodies. In C++ an interface is just an abstract class with only pure virtual functions. Python offers abc.ABC or typing.Protocol.

solid answer

~60 s

The two-way choice is an artefact of nominal, single-inheritance languages such as Java, C# and Kotlin. - **Go**: there is no base class. Interfaces are satisfied structurally and are usually declared by the **consumer**, so the question becomes "what does this caller need?", and shared code lives in an embedded struct rather than a superclass. - **Rust**: traits carry default method bodies and associated types but never fields, and nothing can be inherited from. Shared state is a struct field; shared behaviour is a default or blanket impl. It pays for retroactive impls with the orphan rule. - **C++**: the two collapse — an interface is an abstract class whose functions are all pure virtual. The live decisions are whether to add data members (which forces virtual inheritance in a diamond) and whether to pay for a vtable at all, since templates and CRTP give polymorphism with no base type. - **Python**: `abc.ABC` is nominal and may carry mixin bodies and state; `typing.Protocol` is structural and requires no declaration from the implementer.

code

go · 7 lines
go
// package that CONSUMES the abstraction
type Namer interface{ Name() string }

func greet(n Namer) string { return "hi " + n.Name() }

// any third-party type with Name() string already satisfies it -
// no declaration, no adapter, no edit to that package

go deeper

for a junior

Know that the classic answer depends on the language, and be able to name at least one language with no abstract classes and one where an interface is just an abstract class.

for a middle

Explain what replaces the choice in Go, Rust and C++, and restate the decision as state, identity and who may declare conformance.

for a senior

Use the criteria to choose deliberately in whichever language you are in, including keeping interfaces narrow so they never need to grow.

for a principal

Decide the team's default coupling model — provider-declared nominal contracts versus consumer-declared structural ones — and be explicit about what each costs when you have to retrofit third-party types.

## Why the question exists at all "Abstract class or interface?" is a famous interview question because a specific family of languages makes it a real, irreversible decision. Those languages share three properties: **nominal typing** (a type conforms only if it says so), **single implementation inheritance** (a class may extend at most one class), and a syntactic distinction between a class that can carry state and an interface that cannot. Java, C#, Kotlin and Swift's class side all fit. In that world, choosing an abstract base spends the implementer's one inheritance slot forever, so the choice matters and has to be made up front. Remove any of the three properties and the question dissolves or turns into a different question. ## Go: the consumer declares the abstraction Go has no classes and no inheritance. A type satisfies an interface simply by having the right methods — nothing is declared on the type. The consequence is architectural: interfaces are typically declared **next to the code that consumes them**, not next to the code that implements them, and they are kept tiny (`io.Reader` has one method). Code reuse that would have lived in an abstract base lives in an **embedded struct**, which is delegation with automatic method forwarding, not subtyping. So the Go version of the question is "what is the smallest set of methods my function needs?" — a question about the call site, not about the type hierarchy. You never edit a third-party type to make it fit. ## Rust: traits with defaults, and no inheritance at all Rust has traits, which look like interfaces with default bodies, and structs, which hold data. There is no way to inherit implementation from another struct. So the abstract-base option simply does not exist; the design question becomes "what goes in the trait's default bodies, and what must each implementer supply?" Shared state is expressed by requiring accessor methods, or by having implementers hold a common struct as a field. Rust also allows something Java and C# do not: implementing your trait for a type you did not write. That freedom is bounded by the **orphan rule** — you may not implement a foreign trait for a foreign type — because trait resolution is global and two conflicting impls would be ambiguous. When the rule bites, you wrap the foreign type in a newtype. ## C++: the two are the same construct C++ has no `interface` keyword. An interface is a class whose member functions are all **pure virtual** and which declares no data members. Because C++ permits multiple inheritance of implementation, the compile-time restriction that forces the Java-style choice is absent. The decisions that remain are lower level and sharper: adding a data member to a base means that inheriting it twice produces two subobjects unless the bases are declared `virtual`, which changes construction order and member access; and if you do not need runtime substitution at all, templates or CRTP give you polymorphism with no base class and no vtable. ## Python: pick your coupling model per abstraction Python lets you have both, explicitly. `abc.ABC` gives a nominal abstract base: subclasses must be registered or must inherit, and the base may supply concrete mixin methods and even state. `typing.Protocol` (with `@runtime_checkable` if you need isinstance) gives Go-style structural conformance: the implementer neither knows nor declares anything. The same program can use an ABC where a shared skeleton exists and a Protocol where only a shape is needed. ## What actually drives the decision, once the taxonomy is gone Stripped of language ceremony, three questions remain, and they are the same everywhere: 1. **Is there shared state or a shared algorithm skeleton?** If yes, something must own it — a base class, an embedded struct, a delegated collaborator. This is a code-reuse question. 2. **Is conformance intrinsic to what the type *is*, or is it a role the type can *play*?** Intrinsic identity tolerates a base type; roles want small, separately declared contracts. 3. **Who is allowed to declare conformance, and can it be added later for a type you do not own?** This is where the languages genuinely diverge, and it is often the deciding factor in practice. ## Answering the interview version well A weak answer recites "an interface is a contract, an abstract class shares code, a class can implement many interfaces but extend one class". A strong answer notices that the last clause is a rule of *specific languages*, names Go, Rust, C++ and Python as counter-examples, and then re-derives the decision from state, identity and who may declare conformance — criteria that survive translation to any of them.

  • Go has no abstract classes, yet Go programs clearly reuse implementation. How?
    By embedding: a struct includes another struct or interface as an anonymous field and the outer type automatically forwards the inner type's methods. It reads like inheritance but it is delegation — there is no virtual dispatch back to an override in the outer type, and the outer type is not a subtype of the inner one.
  • If C++ can inherit implementation from many bases, why do C++ style guides still recommend interface-like classes with no data?
    Because data in a base is what turns a diamond into a layout problem: inheriting the same non-virtual base twice yields two copies of its fields, and fixing that with virtual inheritance changes construction order and makes member access indirect. A base with only pure virtual functions and no members can be mixed in freely with none of that cost.
  • What does structural conformance cost compared with declaring `implements`?
    You lose intent. Any type that happens to have a method of the right name and shape satisfies the interface whether or not it means the same thing, and there is no declaration site where the contract's semantics can be documented or reviewed. Nominal conformance is a place to write down and enforce meaning; structural conformance trades that for zero-friction retrofitting.

saying these in an interview costs you the question

  • Stating 'a class can implement many interfaces but extend only one class' as if it were a universal law of OOP rather than a rule of particular languages.
  • Calling Go's struct embedding inheritance; it is delegation with automatic forwarding and gives no subtyping.
  • Claiming Rust traits can hold fields — they can require accessors, but state lives in the implementing struct.
  • Assuming C++ needs an interface keyword or that an abstract class there is a different construct from an interface.
  • Treating typing.Protocol and abc.ABC in Python as interchangeable; one is structural and one is nominal.

context

open as a page

Your library publishes both an abstract base class that outside teams subclass and an interface they implement. Set aside anything to do with default method bodies on the interface: what does shipping the base class commit you to that the interface does not, and how do the languages you know differ when you later add a concrete method or a data member to that base?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Publishing a base spends every subclass's one inheritance slot and turns its constructors and protected members into API. Later adding a concrete method can silently capture a subclass's own method — silent in Java, a hiding warning in C#, a compile error in Kotlin and Swift.

open as a page