6 Interfaces and Abstract Types

Learn how interfaces and abstract classes define contracts, support polymorphism, invert dependencies, and enable replaceable implementations in object-oriented design.

Why abstraction matters

An separates the operations clients can rely on from the representation and implementation that produce those operations. This separation allows code to depend on a stable concept instead of a particular concrete class.

Two common forms are:

  • An , which primarily defines a contract of supported operations.

  • An , which provides a partial implementation for a family of related classes.

An normally cannot be instantiated directly. A concrete type supplies the missing behavior and can then be instantiated.

Takeaway: Abstract types describe a usable concept while leaving implementation details to concrete types.

Interfaces as behavioral contracts

An is a named contract. It tells clients which operations are available and what those operations mean, but it does not require one particular way to produce the behavior.

A complete contract can specify:

  • Operation names and signatures.

  • Valid inputs and the meaning of outputs.

  • Behavioral guarantees.

  • Failure behavior, such as errors or rejected inputs.

  • State changes, input/output activity, and whether an operation is safe to repeat.

For example, a notifier may promise a send operation without deciding whether the concrete notifier uses email, text messaging, or an in-app notification. The contract is stronger than a method name alone: an operation such as withdraw should also clarify how negative amounts, insufficient funds, and balance updates are handled.

Takeaway: A useful explains observable behavior, not merely the names of methods.

Interfaces and abstract classes

An is a partially implemented foundation for related subclasses. It can combine behavior that belongs to every member of the family with operations that vary between subclasses.

An may provide:

  • Shared fields or state.

  • A constructor.

  • Fully implemented methods.

  • Abstract methods that subclasses must implement.

  • Protected operations intended for subclasses.

Choose an when the main goal is to define a capability that may apply to unrelated types. Choose an when related types need shared state, constructors, protected members, or reusable implementation. A type can often implement several interfaces, whereas it usually has one class base.

Takeaway: Interfaces emphasize a capability or contract; abstract classes emphasize a common base and partial implementation.

and dependency inversion

When client code uses an abstraction, different concrete objects can provide different behavior through the same operation. This is . A checkout component, for example, can depend on a payment processor abstraction and work with a card processor, a predictable test processor, or another valid implementation.

This separation also supports . High-level policy should not construct or depend directly on a specific database product or other low-level detail. Instead, the high-level component depends on an abstraction such as an order store, while concrete stores conform to that abstraction.

supplies the selected implementation from outside the consuming class. A constructor, factory, configuration object, or framework can provide a production implementation at the application boundary and an in-memory implementation in a test.

The result is lower coupling: changing the database or test substitute does not require rewriting the high-level service.

Takeaway: Depend on stable abstractions and inject concrete implementations at the boundary.

Substitutability and

Two implementations are interchangeable only when they honor the same behavioral contract. Matching operation names is not enough. For a cache abstraction, an in-memory cache, a shared-server cache, and a test cache may all be valid replacements if they preserve the promised result meanings, failure behavior, and freshness guarantees.

This requirement is closely related to the . A valid replacement should not unexpectedly reject inputs that the abstraction accepts, return incompatible results, weaken guarantees, or introduce surprising side effects.

When designing an abstraction:

  1. Identify what the client actually needs.

  2. Express that need as a small, precise contract.

  3. Implement it with one or more concrete types.

  4. Supply implementations rather than constructing them inside clients.

  5. Test clients with substitutes and test implementations against the contract.

Takeaway: Replaceability depends on behavioral compatibility, not just structural similarity.

Designing effective abstractions

Effective interfaces keep responsibilities focused and expose only what clients need. A reader and a writer are often clearer than one large device that also includes printing, scanning, and faxing operations.

Prefer abstractions that represent a stable concept, support multiple implementations, isolate a volatile dependency, or clarify a component boundary. Avoid interfaces that merely rename one concrete class or expose database sessions, framework-specific types, or internal data structures.

Good abstractions support several design goals:

  • Encapsulation: clients rely on supported operations rather than internal representation.

  • Extensibility: new implementations can be added without rewriting clients.

  • Testability: fakes, stubs, and in-memory implementations can replace external systems.

  • Separation of concerns: high-level policy remains distinct from infrastructure details.

  • Controlled coupling: modules depend on stable contracts instead of volatile classes.

Final takeaway: Design small, meaningful, behaviorally precise abstractions, then preserve their promises across every implementation.