Objects Namespaces And Application Architecture

Polymorphism With Interfaces And Substitutable Objects

Polymorphism lets code use different implementations through one stable contract. In PHP this usually appears through interfaces, inherited methods, callable objects, or framework abstractions. The value is not avoiding every conditional. The value is allowing a caller to depend on behavior without knowing the concrete provider.

Polymorphism lets callers use different implementations through one contract. The contract includes behavior, not only method names and PHP types.

Why This Matters

An implementation can match an interface and still surprise callers by accepting fewer inputs, returning different meanings, leaking provider exceptions, or changing retry behavior. That is why substitutability must be tested.

Start with the object or service that owns the relevant invariant. Identify who supplies input, who may change state, and what result the caller is entitled to observe. For Polymorphism With Interfaces And Substitutable Objects, the most persuasive evidence comes from public behavior, contract tests, dependency direction, and valid state transitions; naming those observations before implementation prevents tool names from standing in for a design.

Working Model

A polymorphic contract has syntax and semantics. Matching a method signature is only the syntax. Each implementation must also accept the promised inputs, preserve the documented guarantees, return compatible outcomes, and fail in ways the caller can handle. This is the practical meaning of substitutability.

The important vocabulary includes:

  • interfaces as behavioral contracts
  • runtime dispatch
  • substitutability
  • composition roots
  • test doubles
  • when a match expression is simpler

Interfaces describe callable shape, implementations provide behavior, the composition root selects concrete objects, and contract tests prove that every implementation honors the same promise.

Core Design Decisions

  1. Introduce an interface when a real boundary has multiple implementations or deserves isolation from infrastructure.
  2. Keep contracts small enough that every implementation can honor every operation without throwing unsupported-operation errors.
  3. Describe semantic guarantees such as idempotency, timeout behavior, or delivery expectations alongside PHP signatures.
  4. Select the concrete implementation in the composition root rather than throughout business code.
  5. Use a fake implementation in tests only when it preserves the relevant contract semantics.
  6. Prefer an explicit conditional for a closed, simple variation when an interface hierarchy would add indirection without flexibility.

Use abstraction where it isolates a real variation or responsibility; otherwise prefer direct, cohesive code. Record the reason beside the code or configuration when the choice is not obvious, especially when future maintainers might otherwise “simplify” away a required guarantee.

PHP-Facing Example

The example uses tax policies because each implementation can honor the same operation. The interface would be weaker if one policy needed to throw for ordinary positive amounts.

PHP example
<?php

declare(strict_types=1);

interface TaxPolicy
{
    public function taxPennies(int $netPennies): int;
}

final class NoTax implements TaxPolicy
{
    public function taxPennies(int $netPennies): int
    {
        return 0;
    }
}

final class PercentageTax implements TaxPolicy
{
    public function __construct(private int $basisPoints) {}

    public function taxPennies(int $netPennies): int
    {
        return intdiv($netPennies * $this->basisPoints, 10_000);
    }
}

function grossPennies(int $net, TaxPolicy $policy): int
{
    return $net + $policy->taxPennies($net);
}

echo grossPennies(10_000, new PercentageTax(2_000));

// Prints:
// 12000

In Polymorphism With Interfaces And Substitutable Objects, notice which values the example accepts and what form it returns. Production code should preserve that clarity when it crosses the object or service that owns the relevant invariant, translating external or loosely typed data at the edge instead of allowing it to spread through unrelated classes.

Implementation Workflow

Introduce an interface when there is a real boundary or multiple coherent implementations. Keep it small, document semantic guarantees, translate provider failures at adapters, and run shared tests against every implementation.

Failure Modes

  • Creating an interface for every class even when there is no boundary or alternative behavior.
  • Implementations returning different meanings for the same method result.
  • A subtype strengthening preconditions so valid base-contract input fails.
  • Leaking provider-specific exceptions through a supposedly provider-neutral interface.
  • Using inheritance only to reuse code while violating the parent behavioral contract.
  • Building mocks that return convenient values impossible for the production implementation.

Watch for interfaces created for every class, subtype-specific conditionals in callers, mocks that cannot happen in production, and implementations that use the same method name for different outcomes.

Verification Strategy

Use the following checks as a starting point:

  • Run one shared contract test suite against every implementation.
  • Test success, rejection, timeout, retry, and malformed-provider-response behavior.
  • Verify dependency wiring chooses exactly one implementation per environment.
  • Review whether callers perform instanceof checks that undermine the abstraction.
  • Measure whether the interface hides materially different performance or consistency characteristics.
  • Confirm test doubles model the outcomes business code actually depends on.

Contract tests should cover success, rejection, provider failure, timeout, repeated calls, and returned result meaning. Wiring tests should prove the intended implementation is selected for each environment.

Security And Data Handling

Keep privileged collaborators explicit and prevent callers from bypassing invariant checks through public mutation or hidden service lookup.

A polymorphic adapter can hide credentials, provider payloads, and derived records. Keep those details at the boundary and expose application-owned result types to callers.

Tradeoffs And Evolution

Use abstraction where it isolates a real variation or responsibility; otherwise prefer direct, cohesive code.

Adding a method to a public interface breaks every implementation. Prefer a separate capability interface when only some implementations support a new operation.

Review Questions

Before considering the topic implemented, answer these questions:

  • Which Polymorphism With Interfaces And Substitutable Objects guarantee matters to the caller?
  • Where does the object or service that owns the relevant invariant live?
  • Which input or state can be stale, malformed, duplicated, or unauthorized?
  • What evidence from public behavior, contract tests, dependency direction, and valid state transitions proves the normal path?
  • Which failure is retryable, and how are duplicate effects prevented?
  • How will public methods, subclass contracts, serialized objects, container wiring, and persistence mapping be handled during change?
  • Which operational signal reveals degradation?
  • What simpler choice was rejected, and why?

If callers immediately branch on concrete type, the abstraction is probably lying. Split the capability or use a direct conditional when the variation is closed and simple.

Production Walkthrough

Compare two payment gateways behind one PaymentGateway interface. Both must define what an authorization reference means, whether capture is idempotent, which errors are permanent, and how ambiguous timeouts are reconciled. A common method signature is not enough.

Write one shared contract suite and run it against both adapters. Add provider-specific tests only after the shared behavior is proven. If one provider cannot refund, do not add refund() to the base interface and make it throw; introduce RefundablePaymentGateway for code that actually needs that capability.

During review, search for instanceof checks against implementations. Each check is evidence that the common contract may be too broad or too vague.

Interfaces At The Right Boundary

An interface is most valuable at a boundary where the application needs to control dependency direction. Payment providers, clocks, mailers, queues, and object storage are common examples. The application owns the vocabulary it needs, and adapters translate external SDKs into that vocabulary.

Do not create an interface for every concrete class automatically. A one-implementation interface that mirrors every method adds indirection without improving substitutability. It can even make refactoring harder because developers now change two files for one local concept. Extract the interface when a second implementation, testing boundary, or dependency direction makes the contract useful.

Test doubles must obey the same semantics as production implementations. A fake queue that immediately calls the handler may hide duplicate delivery, ordering, and retry behavior. A fake payment gateway that always succeeds may hide permanent rejection and ambiguous timeout paths. Good fakes model the outcomes the caller is expected to handle.

Polymorphism can also be provided by callables, strategies, or configuration maps. If the set of variants is small and closed, a match expression may be clearer than an interface hierarchy. The design should make variation understandable, not merely object-oriented.

Review Checks

For every interface, list its implementations and run the same behavioral examples against each one. The examples should include at least one ordinary success, one rejected input, one dependency failure, and one repeated call. Then inspect callers for concrete-type checks. If callers still need to ask which implementation they received, the contract should be narrower, split into capabilities, or replaced with an explicit conditional.

Also compare performance and consistency guarantees. Two cache implementations, search providers, or message transports may share a method while differing in latency, ordering, durability, or visibility delay. Document those differences where callers choose the abstraction.

Change Management

When an abstraction changes, migrate implementations before callers rely on the new behavior. Add a new interface or method in a compatible release, implement it for each concrete class, update wiring and contract tests, and only then move callers. If old and new implementations can run during one deployment, keep result objects and exceptions compatible until every process has moved.

Official References

What You Should Be Able To Do

After this lesson, you should be able to explain polymorphism with interfaces and substitutable objects in operational terms, choose it only when its guarantees fit the requirement, implement a narrow PHP-facing boundary, recognize the common failure modes, and verify behavior using evidence from the real system rather than assuming the configuration is correct.

Practice

Model The Boundary

For Polymorphism With Interfaces And Substitutable Objects, write a short design note for a product-catalog application. Identify the caller, authoritative state, inputs, output, trust boundary, and one invariant. Include one success timeline and one partial-failure timeline.

Show solution

A strong answer names concrete ownership rather than only a tool. The authoritative state should be explicit, and derived copies should be labelled as such. The success timeline should identify when the result becomes observable. The failure timeline should stop after an intermediate step and explain whether retry is safe, whether status must be queried, or whether reconciliation is required.

The invariant should be testable. Examples include “one order causes at most one captured payment,” “only authorized tenant documents appear in results,” or “the latest valid configuration is the one served after rollout.”

Review A Design

Review an implementation of Polymorphism With Interfaces And Substitutable Objects that works in one local demonstration. Use the lesson’s failure modes to identify at least four production risks. For each risk, propose the narrowest change that restores a clear guarantee.

Show solution

The review should connect each risk to a violated guarantee. Good findings cover validation, authorization, retry or duplicate behavior, environment drift, and observability. Avoid broad recommendations such as “use best practices.” Name the responsible boundary and the evidence that would prove the repair.

A narrow repair may be a constraint, explicit comparison policy, interface contract, timeout, idempotency key, index, security rule, probe, IAM policy, or integration test. The selected mechanism must match the actual risk.

Build A Verification Plan

Create a verification plan for Polymorphism With Interfaces And Substitutable Objects. Include one unit test, one boundary-level integration test, one negative security or validation test, one failure-injection exercise, and two operational signals. State what each check proves and what it does not prove.

Show solution

The unit test should cover a pure decision. The integration test must cross the real boundary rather than only checking a prepared request. The negative test should use an identity or input that must be rejected. The failure exercise should create a timeout, retry, restart, unavailable dependency, or incompatible version deliberately.

Operational signals should reveal both health and correctness. Useful examples include latency percentiles, error classification, queue age, indexing lag, denied requests, restart count, duplicate side effects, resource saturation, and final business-state reconciliation.