5 Polymorphism and Substitutability

Learn how polymorphism, dynamic dispatch, interfaces, and behavioral contracts enable flexible object-oriented designs whose implementations can be substituted safely.

Through Common Abstractions

lets client code work with different concrete objects through one shared abstraction. The client depends on the operations guaranteed by the abstraction, while each supplied object provides its own implementation.

For example, a notification method can accept a Notifier rather than a particular email or SMS class. The method can request send without testing which concrete notifier it received. Adding another notifier implementation can therefore extend the system without changing the notification method.

This design separates two concerns:

  • The abstraction defines the contract that client code may rely on.

  • The concrete implementation determines how that contract is fulfilled.

A useful rule is to write client code against the narrowest useful type. If a method only needs the ability to send a message, accepting Notifier is more flexible than accepting EmailNotifier.

Takeaway: makes behavior variable behind a stable contract, reducing the need for concrete-type checks and making extension easier.

and Runtime Behavior

When a reference points to an object whose concrete type is more specific, an overridable instance-method call can use the object's actual implementation. This is .

Suppose a variable has declared type Shape, but the object assigned to it is a Circle. A call to area() is checked through the Shape reference, then resolved at runtime to the Circle implementation. The declared type determines what operations are available to the client; the runtime type determines which overridden implementation executes.

This differs from compile-time selection:

  • Overloaded methods are selected using compile-time information such as the method name and argument list.

  • Static methods do not participate in ordinary instance-.

  • Overridden instance methods can be selected according to the object's runtime type.

Takeaway: The reference type controls the visible contract, while can select the concrete behavior at runtime.

Overriding, Overloading, and Hiding

allows a subtype to provide specialized behavior for an inherited instance operation. The overriding method must have a compatible signature and must preserve the meaning promised by the inherited operation.

In Java, the @Override annotation asks the compiler to verify that an inherited method is actually being overridden. This prevents a misspelled name or incorrect parameter list from silently creating a separate method. Java also permits a covariant return type, in which the overriding method returns a subtype of the original return type.

Overriding is different from overloading:

  • Overriding replaces inherited instance behavior in a subtype and participates in runtime dispatch.

  • Overloading creates methods with the same name but different parameter lists and is primarily resolved from compile-time argument information.

Method hiding is different from overriding as well. If a language's hiding mechanism creates an unrelated method with the same name, the selected behavior may depend on the variable's declared type rather than the object's runtime type.

Takeaway: Use the language's overriding mechanism when the goal is subtype-specific behavior through a common reference.

and Behavioral Contracts

A subtype is useful only when clients can safely treat it as an instance of the abstraction. This is the practical meaning of . Structural compatibility—such as having a method with the right name—is not enough; the implementation must preserve the behavior clients expect.

A substitutable implementation should:

  • Accept every input that the abstraction promises to accept.

  • Provide the result or effect promised by the abstraction.

  • Preserve important state rules, ordering guarantees, and other invariants.

  • Avoid unexpected restrictions, failures, or side effects.

For example, if a FileStore abstraction accepts any nonempty filename, an implementation that rejects filenames containing spaces is not substitutable unless that restriction is part of the abstraction's contract. The implementation may use a different storage technology, but its observable behavior must remain compatible.

The expresses this idea as a behavioral requirement: properties clients can rely on for a supertype must remain valid when a subtype is substituted.

Takeaway: A subtype must honor the abstraction's promises, not merely inherit from its base class or match its method signatures.

Testing Contracts with

Contracts can be examined by comparing what an abstraction requires before an operation and guarantees afterward. provide a practical test for whether a subtype remains substitutable.

A subtype should not strengthen a precondition. If the abstraction accepts a category of inputs, the subtype should not reject valid members of that category without a contract-based reason. In other words, the subtype should not require more from its clients than the supertype does.

A subtype should preserve or strengthen the postcondition. It must provide at least the result, effect, or guarantee that clients expect from the abstraction. It should also preserve important invariants, such as valid state relationships or ordering guarantees.

A useful review sequence is:

  1. List the inputs clients are allowed to provide through the abstraction.

  2. Check whether the subtype accepts all of those inputs.

  3. List the results, effects, and failures the abstraction promises.

  4. Check whether the subtype preserves those promises.

  5. Check whether state and ordering rules remain valid.

Takeaway: can be tested by ensuring that subtypes do not demand more, promise less, or break the abstraction's invariants.

Interfaces and Focused Capabilities

An interface makes a capability explicit. define what clients can request without forcing them to depend on how a particular class performs the operation.

For example, a TaxCalculator interface can expose calculate, while separate implementations can apply a standard tax rule or return no tax for an exempt case. A total-calculation method can depend only on TaxCalculator, so its code remains unchanged when another valid calculation strategy is introduced.

Focused interfaces improve because they give implementations a small, clear promise to fulfill. An overly broad interface can force a class to provide meaningless operations and can make the contract difficult to honor.

Interfaces are especially useful when:

  • Unrelated classes should provide the same capability.

  • A class needs to conform to several abstractions.

  • Client code should depend on stable behavior rather than implementation details.

  • A test double or alternate implementation should be easy to supply.

Takeaway: A focused interface turns a capability into an explicit contract and reduces coupling between clients and implementations.

Designing Code Around Abstractions

Polymorphic behavior can also be supplied through composition. Instead of making a report generator inherit from a formatter, the report generator can contain a Formatter object and delegate formatting to it.

This approach allows a JsonFormatter, TextFormatter, or test double to be supplied without modifying the report generator. The generator depends on the formatter's capability, not on a particular formatting algorithm or inheritance relationship.

Good design practices include:

  • Accept the narrowest useful abstraction in method parameters.

  • Return abstractions when practical, allowing factories to hide implementation choices.

  • Replace repeated concrete-type tests with an interface operation or overridden method when behavior belongs to the object.

  • Document valid inputs, results, side effects, and failure behavior.

  • Use inheritance only when the subtype represents a genuine behavioral relationship.

  • Prefer composition when behavior varies independently of the object's identity.

Takeaway: Dependence on stable capabilities, combined with precise contracts and composition where appropriate, produces flexible designs without fragile subclass hierarchies.