6 Interfaces and Abstractions
A progressive guide to interfaces, abstract classes, polymorphism, dependency direction, and the design of focused abstractions in object-oriented software.
The purpose of
An focuses attention on what a component can do rather than on every detail of how it does it. This creates a separation between a public and an .
For example, a notification client may require only the ability to send a message. It should not need to know whether the message is delivered by email, text message, or another mechanism. Several unrelated classes can provide that capability through the same .
This separation is valuable when implementations may change, when infrastructure should be isolated from business logic, or when tests need substitute components.
Takeaway: Depend on stable capabilities, not on details that are likely to vary.
Interfaces as focused contracts
An defines required behavior without normally prescribing how that behavior is implemented. A client can accept an -typed parameter and call the promised operation without selecting a particular concrete class.
A class may implement several interfaces, which allows one object to support independent capabilities. For instance, a document, photograph, and map could all implement a Printable even though they do not belong to the same conceptual class hierarchy.
In a nominal type system, a class explicitly declares that it implements an . In a structural type system, a type qualifies when it has the required members. is the name for this latter relationship; it is also associated with static duck typing.
An is strongest when it is cohesive and small. If unrelated clients need different operations, splitting one large into focused interfaces usually produces clearer dependencies.
Takeaway: Use an to express a meaningful capability or boundary, especially when unrelated types may provide that capability.
Abstract classes and related families
An is an incomplete class intended to serve as a base for related concrete classes. It may contain abstract members that subclasses must provide, concrete members that supply shared behavior, and state or protected operations used by that shared behavior.
An is useful when subclasses share more than a capability. For example, a report-exporting base class could format a report and control the overall export process while leaving the format-specific output operation to concrete subclasses.
Interfaces and abstract classes both express contracts, but they emphasize different relationships:
Use an when the capability may apply to unrelated classes.
Use an when one class needs several independent capabilities.
Use an when subclasses form a closely related family.
Use an when subclasses should share state, construction rules, invariants, or substantial .
Use an when the base class should control part of an algorithm.
Inheritance should not be chosen solely to reuse a small piece of code. If an object needs a formatter, policy, or service, composition and may communicate the design more clearly.
Takeaway: Choose an for a capability and an for a related family that shares or state.
across implementations
allows one client to work with multiple concrete types through the same . A function that calculates totals for priced items can rely only on a promised pricing operation; the items might be books, subscriptions, or service plans.
The client remains unchanged while each concrete object calculates its result differently. The appropriate is selected for the actual object at runtime.
This approach reduces conditional logic based on concrete class names. Instead of asking which kind of object has been supplied and branching for each case, the client invokes the operation defined by the .
is the concrete type that fulfills the 's . For to be reliable, implementations must satisfy not only the required signatures but also the expected semantics, including relevant error behavior and invariants.
Takeaway: lets clients depend on stable behavior while concrete implementations vary.
direction and injection
A direct concrete can make a high-level component difficult to replace or test. If an order service constructs one specific payment gateway internally, it becomes tied to that vendor, construction process, and possibly network behavior.
changes the design direction: important application logic depends on a payment , while a concrete gateway implements that . supplies the gateway from outside the service.
The supplied can be a production gateway in the application or a fake payment gateway in a test. The service can therefore be tested in isolation without changing its business logic.
A layered design can follow this pattern:
The user calls an application service.
The application service depends on a repository .
A database-specific repository implements that .
This arrangement improves replaceability, testability, maintainability, and modularity. It also keeps database drivers, SQL details, and vendor-specific behavior outside the business logic.
Takeaway: Let high-level policy depend on contracts and supply infrastructure implementations at the boundary.
Designing useful abstractions
means expressing a component's real requirements with the most general useful type. A function that only needs to iterate over documents should accept an iterable document rather than a particular array-list .
A practical design process is:
Identify what the client actually needs.
Define a small for those operations.
Make concrete types fulfill the .
Pass implementations into the client.
Keep -specific details outside the client.
Do not create an merely because every class is expected to have one. An earns its place when it represents a meaningful variation, capability, boundary, or . An with too many members is difficult to implement, while one with too few meaningful members may not help clients.
A includes more than method names and parameter types. For a cache, clients may need to know whether a missing key produces a special result or an exception, whether values are copied or referenced, whether access is thread-safe, and what happens when storage is full. Matching signatures do not guarantee matching behavior.
Takeaway: Keep abstractions focused, behaviorally precise, and independent of unnecessary technology details.
Putting the principles together
Interfaces and abstract classes provide different ways to separate contracts from implementations. Interfaces emphasize capabilities that may cross unrelated class hierarchies, while abstract classes support related types that share state or algorithmic structure.
makes those contracts useful at runtime: clients can work with different implementations without changing their own logic. and extend the same idea across architectural boundaries, keeping application policy independent from infrastructure.
A durable should be:
Small enough for clients and implementers to understand.
Meaningful enough to support realistic implementations.
Explicit about important semantics and error behavior.
Free from unnecessary database, framework, file-format, or vendor details.
Tested with more than one realistic when the is intended to support variation.
The central design question is not whether a class should have an . It is whether the client needs a stable, meaningful that separates required behavior from changeable details.