Interfaces & Abstract Classes
Both define contracts that other classes fulfill. An interface is a pure contract; an abstract class is a partial implementation you extend.
Contracts vs partial classes.
An interface lists methods a class must provide, without saying how — like a job description. A class that implements the interface promises to supply those methods.
An abstract class is a base class you can't instantiate directly; it can provide some ready-made methods and leave others abstract for subclasses to fill in.
# Problem: checkout hard-codes one payment type. class Checkout: def charge(self, card, amount): card.charge_credit_card(amount) # can't plug in PayPal / wallet
# Fix: define a contract (ABC). Any class that implements pay() works. from abc import ABC, abstractmethod class PaymentMethod(ABC): @abstractmethod def pay(self, amount): """Charge `amount`. Subclasses must implement this.""" class CreditCard(PaymentMethod): def pay(self, amount): print(f"charging card {amount}") class PayPal(PaymentMethod): def pay(self, amount): print(f"paypal {amount}") def checkout(method: PaymentMethod, amount): # Depends on the contract, not a concrete class. method.pay(amount) checkout(CreditCard(), 20) checkout(PayPal(), 20)
Interface vs abstract class.
| Interface | Abstract class | |
|---|---|---|
| Implementation | Traditionally none (now default methods) | Can provide shared code |
| State/fields | Only constants | Can hold instance state |
| Multiple? | A class can implement many | Usually only one parent |
| Relationship | "can-do" capability | "is-a" specialization |
Default methods and beyond.
- Default methods — interfaces can carry implementation (Java 8+), blurring the line and enabling backward-compatible evolution.
- Interface Segregation — prefer many small, focused interfaces over one fat one.
- Program to an interface — depend on abstractions for loose coupling and testability.
- Evolution — adding to an interface can break implementers unless defaulted.
Interview Questions
A contract that declares a set of method signatures (capabilities) without implementations. Classes that implement the interface must provide those methods, allowing them to be used interchangeably through the interface type.
A class that cannot be instantiated on its own and is meant to be subclassed. It can define concrete (implemented) methods and shared state as well as abstract methods that subclasses must implement.
No, not directly — both are incomplete. You instantiate a concrete class that implements the interface or extends the abstract class, and you can reference that object through the interface/abstract type.
Yes. A class can implement any number of interfaces (gaining multiple capabilities), even though it can usually extend only one (abstract) class. This is how languages like Java allow multiple inheritance of type without multiple inheritance of implementation.
In Java, a class implements an interface and extends a (abstract) class. C# uses : for both. The distinction reflects "fulfilling a contract" vs "specializing a base class."
Comparable (defines compareTo so objects can be sorted), Iterable (can be looped over), or a Repository interface that different data-store implementations satisfy. Any type implementing them can be used by generic code expecting that capability.
Typically "can-do" (a capability/role): a class that implements Serializable "can be serialized." An abstract class usually represents "is-a" (a kind of): a Dog is an Animal.
An abstract class can hold instance state (fields) and provide concrete method implementations plus constructors, and a class can extend only one. An interface (traditionally) declares only method signatures and constants (no instance state), and a class can implement many. Semantically, abstract classes model an "is-a" specialization with shared code; interfaces model a "can-do" capability. Use an abstract class for closely related types sharing implementation, an interface for a contract many unrelated types can fulfill.
When you have a family of closely related classes that share common state and/or default behavior you want to implement once (e.g. a base Shape storing position and providing a common move(), while area() is abstract). Abstract classes also let you add non-abstract methods later without breaking subclasses. Choose an interface instead when unrelated types need a common capability, when you need multiple inheritance of type, or when you want maximum decoupling.
Default methods (Java 8+) let an interface provide a method body. They were added mainly to evolve interfaces without breaking existing implementers — e.g. adding stream()/forEach() to Collection with defaults so old code still compiles. They also enable sharing convenience logic. The downside is they blur the interface/abstract-class distinction and can create multiple-inheritance ambiguity when two interfaces define the same default.
Declare variables, parameters, and return types using the most general interface/abstract type rather than a concrete class (e.g. List not ArrayList, a PaymentGateway interface not StripeGateway). This decouples code from specific implementations, so you can swap them (including test doubles) without changing callers, and it clarifies the required capability. It's foundational to loose coupling, testability, and dependency injection.
An interface with no methods, used purely to tag a class with metadata that code or the runtime checks (e.g. Java's Serializable, Cloneable). The presence of the type signals a capability/intent. Modern code often prefers annotations/attributes for this purpose, but marker interfaces still enable type-based checks (instanceof) and generic bounds.
Yes to both. An interface can extend one or more interfaces to compose a larger contract. An abstract class can implement an interface and provide some or all of its methods (leaving the rest abstract for subclasses). These let you build layered contracts and partial base implementations — e.g. an abstract AbstractList implements the List interface with common logic, and concrete lists extend it.
DI supplies a class's dependencies from outside, typically typed as interfaces. Depending on interfaces (not concrete classes) is what makes injection useful: the container/caller can provide any implementation (real, alternate, or a mock for tests) without the dependent class changing. So interfaces define the seams and DI wires concrete implementations into them, together achieving loose coupling and testability.
ISP says clients shouldn't be forced to depend on methods they don't use — so prefer several small, role-focused interfaces over one large "fat" interface. A fat interface forces implementers to provide (or stub out) irrelevant methods and couples clients to changes they don't care about. Splitting it (e.g. separate Readable, Writable, Closeable instead of one big Stream) lets each client depend only on what it needs, reduces the ripple of changes, and avoids empty/throwing implementations. The classic smell is a class implementing an interface with methods it can't meaningfully support.
Every existing implementer of the interface would suddenly be missing the new method and fail to compile (source break) or fail at link/runtime (binary break) — you can't add to a contract others have already fulfilled without their cooperation. Avoidance strategies: add the method as a default method so existing implementers inherit a behavior; create a new interface (e.g. FooV2) that extends the old one and have new code depend on it; or use abstract classes with non-abstract additions. In general, treat published interfaces as immutable contracts, design them minimally, and evolve via extension rather than modification.
If a class inherits the same default method from two interfaces, there's an ambiguity. Java resolves it with rules: a class's own method or a superclass method wins over interface defaults; a more specific sub-interface's default wins over a parent's; and if there's a genuine conflict, the class is forced to override and can explicitly pick one with InterfaceName.super.method(). This keeps multiple inheritance of behavior safe by requiring explicit disambiguation rather than silently choosing. Other languages (Scala traits with linearization, C++ virtual inheritance) use their own precedence rules to the same end.
Nominal typing (Java, C#): a type satisfies an interface only if it explicitly declares that it implements it — the name/declaration matters. Structural typing (TypeScript, Go): a type satisfies an interface if it simply has the required members, regardless of any declared relationship ("if it has the shape, it fits"). Structural typing (like statically-checked duck typing) is more flexible and enables retroactive conformance without touching the type, while nominal typing makes intent explicit and prevents accidental matches. Go's interfaces are a well-known structural example; this affects how decoupled and reusable your abstractions are.
Creating an interface for every class "just in case" (especially single-implementation interfaces) adds indirection and files without real benefit, obscuring behavior and violating YAGNI. It can make navigation and debugging harder (jumping through layers to find the real code) and give a false sense of flexibility. Interfaces earn their keep when there are (or credibly will be) multiple implementations, a need for test doubles at a real seam, a published API boundary, or a need to decouple layers. Otherwise a concrete class is simpler; you can extract an interface later when a second implementation actually appears (rule of three).
Template Method typically uses an abstract class: the base defines the invariant algorithm skeleton in a concrete (often final) method and delegates the varying steps to abstract/hook methods that subclasses implement — reuse via inheritance. Strategy typically uses an interface: the varying algorithm is a separate object implementing a strategy interface, injected into the context — reuse via composition, swappable at runtime. Template Method binds variation at subclass-definition time (compile-time, single inheritance), while Strategy binds it at runtime and avoids inheritance coupling. The choice mirrors the broader "composition vs inheritance" trade-off and often Strategy is favored for flexibility.
C++ expresses interfaces as abstract base classes with pure virtual methods (= 0) and no data — a class inheriting it must implement them; multiple such bases give multiple "interfaces." Modern C++ also uses concepts/templates for compile-time structural constraints (duck typing checked at compile time). Python uses Abstract Base Classes (the abc module) to define required methods, or Protocols (typing.Protocol) for structural typing, but often relies on plain duck typing — any object with the right methods works, no declaration needed. So the "contract" idea is universal; the enforcement mechanism (pure virtual base, ABC, protocol, or convention) varies by language.
Traditionally an interface can only declare constants (static final values), not instance fields, because it holds no per-object state. An abstract class, by contrast, can declare instance fields. This is a core structural difference between the two.
Yes. Even though you can't instantiate it directly, an abstract class can define constructors that run when a subclass is created (via super(...)) to initialize its shared state. Interfaces (traditionally) cannot have constructors since they have no state.
Yes — a concrete class must implement every method the interface declares (except those with default implementations). If it doesn't implement them all, it must itself be declared abstract, leaving the rest for its subclasses.
Static interface methods (Java 8+) provide utility/factory helpers related to the interface without needing a separate helper class — e.g. Comparator.comparing(...). They belong to the interface type, not instances, aren't inherited by implementers, and keep related factory logic close to the abstraction it produces.
An interface with exactly one abstract method (e.g. Runnable, Comparator, Function), which can be implemented concisely with a lambda or method reference. It bridges OOP and functional style: the lambda is an instance of the interface. Java marks them with @FunctionalInterface to enforce the single-method rule.
Because the dependency is an interface, you inject a test double implementing it — a stub returning canned values, a mock verifying interactions, or a fake with a simple in-memory implementation. This isolates the unit from real, slow, or nondeterministic collaborators (databases, networks). The interface is the seam that makes substitution possible, which is a primary practical benefit of programming to interfaces.
No. Single-implementation interfaces created reflexively add indirection and files without real benefit and can obscure behavior (the "one interface, one impl" smell). Justified reasons for an interface: multiple real implementations exist or are imminent, you need a test seam at a genuine boundary (I/O, external services), you're publishing a stable API contract, you must decouple layers or break a dependency cycle, or you need dynamic proxying/DI. Otherwise, prefer a concrete class and extract an interface later when a second implementation actually appears (rule of three). Over-interfacing is a form of speculative generality (YAGNI).
Historically, abstract classes were safer to evolve than interfaces: you could add a new concrete method to an abstract base without breaking subclasses, whereas adding a method to an interface broke every implementer. Default methods narrowed this gap by letting interfaces add defaulted methods compatibly. But abstract classes still consume the single inheritance slot and can impose constructor/state constraints. For a widely-implemented public contract, plan for evolution: keep interfaces minimal, add via default methods or new sub-interfaces, and avoid changing existing signatures. The compatibility trade-off often drives the choice for library/framework APIs.
Define a small, stable plugin interface (the contract every plugin must satisfy) plus any host-provided service interfaces the plugin can call back into. The host discovers implementations at runtime (via a service loader / DI registry / dynamic class loading), depends only on the interface (never on concrete plugin classes), and interacts through it — so plugins can be added or replaced without recompiling the host (Open/Closed). Version the interface carefully (it's a public contract), use default methods to evolve it without breaking existing plugins, isolate plugins for stability/security (classloaders, sandboxing), and define clear lifecycle hooks (init/start/stop). This is interface-driven decoupling and dependency inversion at the architectural level.