Objects Namespaces And Application Architecture

SOLID Principles As Design Heuristics

SOLID is a set of design heuristics for managing change. The principles are useful when they clarify responsibility and dependency direction; they are harmful when treated as ceremony.

Why This Matters

A class can look organized and still be hard to change because it mixes reasons for change, exposes unstable abstractions, or depends directly on volatile infrastructure. SOLID helps name those pressures.

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 SOLID Principles As Design Heuristics, 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

Apply SOLID from a concrete change pressure. Ask which decisions change together, which variation is likely, what contract callers rely on, whether clients depend on methods they do not use, and whether policy points inward toward stable abstractions. A principle earns its keep only when it makes a real change or test clearer.

The important vocabulary includes:

  • single responsibility principle
  • open-closed principle
  • Liskov substitution principle
  • interface segregation principle
  • dependency inversion principle
  • cohesion and coupling

Read each principle as a review question: single responsibility asks why a class changes, open/closed asks how behavior extends, Liskov asks whether substitutions are honest, interface segregation asks whether clients depend on unused operations, and dependency inversion asks who owns boundary contracts.

Core Design Decisions

  1. Define responsibility as a reason to change, not as one method per class.
  2. Use extension points for demonstrated variation while keeping straightforward code straightforward.
  3. Require subtypes and implementations to preserve the promises of the contract they replace.
  4. Split broad interfaces when clients are forced to depend on unrelated operations.
  5. Make business policy depend on stable application-owned abstractions at infrastructure boundaries.
  6. Balance abstraction cost against the frequency and risk of the changes it supports.

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 should be read as a dependency-direction exercise, not a rule that every class needs an interface.

PHP example
<?php

declare(strict_types=1);

interface ReceiptSender
{
    public function send(string $email, string $receipt): void;
}

final class Checkout
{
    public function __construct(private ReceiptSender $receipts) {}

    public function complete(string $email, int $totalPennies): void
    {
        $receipt = 'Paid ' . $totalPennies . ' pennies';
        $this->receipts->send($email, $receipt);
    }
}

final class ConsoleReceiptSender implements ReceiptSender
{
    public function send(string $email, string $receipt): void
    {
        echo $email . ': ' . $receipt;
    }
}

(new Checkout(new ConsoleReceiptSender()))->complete('sam@example.com', 2499);

// Prints:
// sam@example.com: Paid 2499 pennies

In SOLID Principles As Design Heuristics, 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

Apply SOLID after naming the concrete change pressure. Extract a class, interface, or collaborator only when it removes a real reason for change or protects a real boundary. Keep direct code when requirements are stable and local.

Failure Modes

  • Turning one readable service into dozens of one-line classes solely to claim single responsibility.
  • Adding strategy interfaces before a second behavior or credible variation exists.
  • Using inheritance where a subtype rejects inputs or changes outcomes promised by its parent.
  • Creating a universal repository interface with methods some stores cannot support.
  • Treating a framework service container as dependency inversion by itself.
  • Citing a SOLID acronym without naming the concrete maintenance problem.

Common failures include one service owning unrelated policies, abstract factories around one implementation, interfaces that mirror every class, and abstractions that force callers to know implementation details anyway.

Verification Strategy

Use the following checks as a starting point:

  • Describe each class reason to change in one sentence and look for unrelated causes.
  • Trace dependency direction from domain policy to infrastructure adapters.
  • Run shared contract tests for substitutable implementations.
  • Search for interfaces implemented with unsupported methods or empty bodies.
  • Review the next likely feature and estimate which modules it would touch.
  • Compare the design with a simpler version and retain abstractions only when their benefit is explicit.

Test behavior before refactoring and after it. Then review whether the new design makes the next expected change smaller, whether dependencies point toward application-owned contracts, and whether implementations remain substitutable.

Security And Data Handling

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

Design boundaries also carry data responsibilities. A dependency inversion that hides a provider should prevent provider payloads, credentials, and exception types from leaking into domain code.

Tradeoffs And Evolution

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

SOLID decisions should be revisited when a second implementation appears, a class changes for repeated unrelated reasons, or a boundary becomes operationally expensive. Do not keep abstractions that no longer protect anything.

Review Questions

Before considering the topic implemented, answer these questions:

  • Which SOLID Principles As Design Heuristics 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?

A SOLID review should end with a specific change that became safer or simpler. If the only benefit is that the code sounds more patterned, the design probably moved in the wrong direction.

Design Review In Practice

Take an invoice service that calculates totals, renders PDFs, stores files, and sends email. The single responsibility issue is not the number of methods; it is the number of reasons the class changes. Split calculation, rendering, storage, and notification when those policies change independently.

For dependency inversion, define an application-owned storage interface only if storage choice is volatile or test isolation matters. A one-line value formatter does not need an interface just to satisfy a checklist.

For Liskov, run the same expectations against every implementation. If a PdfRenderer sometimes returns a filename and another returns bytes, the abstraction is not stable enough for callers.

Applying Principles Without Ceremony

Use single responsibility to find reasons for change, not to split every method into a class. A report generator that queries data, formats totals, renders HTML, writes files, and sends mail probably has several reasons to change. A value object with a few validation helpers may have one reason to change and does not need artificial separation.

Use open/closed when new behavior is expected and can be added safely through a stable boundary. If the variation is speculative, a direct implementation is easier to read. Premature extension points create compatibility obligations before the team understands the real changes.

Use interface segregation when clients are forced to depend on operations they cannot use. A worker that only needs put() should not receive a storage interface that also deletes buckets and changes permissions. Narrow contracts reduce both accidental power and testing burden.

Use dependency inversion to keep domain code independent from volatile infrastructure. The important part is ownership of the abstraction. Depending on a provider SDK interface may still point the dependency in the wrong direction if provider concepts now shape the domain. Define application-owned contracts when the boundary matters.

Review Checks

Tie each SOLID suggestion to a likely change. For example, "move email delivery behind an application-owned port because providers may change" is reviewable; "apply dependency inversion" is too vague. After refactoring, compare the next expected change before and after the design. The principle has helped only if ownership, testing, or replacement became clearer without obscuring the normal path.

SOLID does not remove the need for domain language. Names such as manager, helper, and processor often hide responsibility. Prefer names that state the policy or workflow being protected, then choose an abstraction only if the name still represents one coherent concept.

Balancing Principles

The principles can conflict. A narrower interface may create more types; a direct dependency may be clearer than an abstraction around one stable class; splitting responsibilities can make a workflow harder to read. Prefer the design whose tradeoff is easiest to explain for the next expected change. Record that tradeoff in comments or an architecture note when future reviewers are likely to revisit it.

Official References

What You Should Be Able To Do

After this lesson, you should be able to explain solid principles as design heuristics 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 SOLID Principles As Design Heuristics, 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 SOLID Principles As Design Heuristics 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 SOLID Principles As Design Heuristics. 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.