zhuang@linux:~/notes/software-design/ood-principles-solid/$ cat

SOLID Principles of Object-Oriented Design

$ grep tags ood-principles-solid.md

SOLID is an acronym for five principles of object-oriented design. Robert C. Martin presents these principles in Agile Software Development: Principles, Patterns, and Practices as guidelines for managing dependencies and keeping software easier to change.

At a Glance

LetterPrincipleMain concern
SSingle-Responsibility PrincipleGive a class one reason to change.
OOpen-Closed PrincipleAdd behavior without modifying stable code.
LLiskov Substitution PrincipleMake subtypes safely substitutable for their base types.
IInterface Segregation PrincipleKeep clients from depending on methods they do not use.
DDependency-Inversion PrincipleMake high-level policy depend on abstractions rather than details.

The principles are related. SRP and ISP separate responsibilities, OCP guides extension, LSP keeps abstractions trustworthy, and DIP controls the direction of dependencies. Together, they help contain the effect of change.

Single-Responsibility Principle

A class should have only one reason to change.

The Single-Responsibility Principle (SRP) says that a class should be responsible to one actor or source of change. Responsibility does not mean that a class must contain only one method. It means that its methods should belong to one cohesive purpose.

Consider an Employee class that calculates pay, stores employee records, and produces reports. These behaviors may change for different reasons:

  • Accounting rules change payroll calculations.
  • Database administrators change persistence details.
  • Management changes report formatting.

Keeping all three concerns in one class couples unrelated changes. They can instead be separated behind focused types:

cpp
class PayCalculator {
public:
    Money calculate(const Employee& employee) const;
};

class EmployeeRepository {
public:
    void save(const Employee& employee);
};

class EmployeeReport {
public:
    std::string render(const Employee& employee) const;
};

SRP improves cohesion and reduces the chance that one change will disturb unrelated behavior. It should not be interpreted as “one class per operation.” Splitting a cohesive concept into many tiny classes can make a design harder to understand without isolating any meaningful source of change.

Open-Closed Principle

Software entities should be open for extension but closed for modification.

The Open-Closed Principle (OCP) says that a module should allow new behavior to be added without repeatedly editing its stable core. The usual technique is to identify the dimension that varies and represent it with an abstraction.

A checkout service that selects payment behavior with a growing conditional must be modified for every new payment method:

cpp
if (method == "card") {
    // Process a card.
} else if (method == "bank-transfer") {
    // Process a bank transfer.
}

Depending on a payment abstraction moves that variation outside the checkout policy:

cpp
class PaymentMethod {
public:
    virtual ~PaymentMethod() = default;
    virtual Receipt pay(Money amount) = 0;
};

class CheckoutService {
public:
    Receipt checkout(PaymentMethod& payment, Money amount) {
        return payment.pay(amount);
    }
};

A new implementation can extend the system without changing CheckoutService. The class is not closed against every possible change; it is closed against the particular variation represented by PaymentMethod.

OCP does not justify predicting every future extension. Abstractions have a cost and should normally emerge from real variation. Protect the parts of the system that are stable or expensive to modify, rather than making every class extensible in every direction.

Liskov Substitution Principle

Subtypes must be substitutable for their base types.

The Liskov Substitution Principle (LSP) requires code that works with a base type to continue working correctly when given any of its subtypes. A subtype must preserve the behavioral contract of its parent, not merely share its method signatures.

A subtype violates LSP when it:

  • Requires stronger preconditions than the base type
  • Provides weaker postconditions than the base type
  • Breaks invariants promised by the base type
  • Throws errors for valid operations supported by the base type
  • Changes behavior in a way that surprises clients of the abstraction

The classic rectangle and square example illustrates the problem. If a mutable Rectangle allows width and height to be changed independently, a Square cannot preserve that contract while also preserving equal sides. Inheritance may look mathematically correct, but the types are not behaviorally substitutable.

cpp
void resize(Rectangle& rectangle) {
    rectangle.setWidth(5);
    rectangle.setHeight(4);
    assert(rectangle.area() == 20);
}

Passing a Square that forces both dimensions to remain equal makes the assertion fail. A better model may use immutable shapes with a shared area interface instead of making Square inherit mutable rectangle behavior:

cpp
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

LSP is what makes polymorphism reliable. If clients need type checks or special cases for particular implementations, the abstraction or its subtype contracts should be reconsidered.

Interface Segregation Principle

Clients should not be forced to depend on methods they do not use.

The Interface Segregation Principle (ISP) favors small, client-specific interfaces over large interfaces that serve many unrelated needs. A broad interface couples every client to all of its operations, even when a client uses only one of them.

For example, not every machine that prints can also scan and fax:

cpp
class Printer {
public:
    virtual ~Printer() = default;
    virtual void print(const Document& document) = 0;
};

class Scanner {
public:
    virtual ~Scanner() = default;
    virtual Image scan() = 0;
};

A simple printer implements only Printer, while a multifunction device can implement both interfaces. Neither device needs dummy methods or “unsupported operation” errors.

Signs that an interface may be too broad include:

  • Implementations with empty methods
  • Methods that always throw “not supported”
  • Clients that mock many unrelated methods in tests
  • Frequent interface changes caused by unrelated consumers

An interface should be cohesive from the perspective of its clients. ISP does not mean that every interface must contain exactly one method; related operations used by the same clients can remain together.

Dependency-Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

The Dependency-Inversion Principle (DIP) separates high-level policy from low-level implementation details. Business rules should not directly depend on a particular database, web service, user interface, or framework. Both sides should communicate through an abstraction shaped by the policy’s needs.

cpp
class OrderRepository {
public:
    virtual ~OrderRepository() = default;
    virtual void save(const Order& order) = 0;
};

class CheckoutService {
public:
    explicit CheckoutService(OrderRepository& orders)
        : orders_(orders) {}

    void checkout(const Order& order) {
        // Apply business rules.
        orders_.save(order);
    }

private:
    OrderRepository& orders_;
};

CheckoutService contains high-level checkout policy and depends on the OrderRepository abstraction. A SQL repository is a detail that implements that abstraction:

text
CheckoutService ─────▷ OrderRepository ◁───── SqlOrderRepository
      policy              abstraction                 detail

The source-code dependency points from the database detail toward the policy-owned abstraction, reversing the traditional dependency in which business logic imports database code.

Dependency injection is one way to supply an implementation, but injection and inversion are not the same thing. Passing a concrete SQL repository into a constructor changes object creation without necessarily applying DIP. The important point is which module owns the abstraction and which direction the source dependencies point.

How the Principles Work Together

The five principles address different design pressures:

Design pressureRelevant principle
A class changes for unrelated stakeholdersSRP separates the responsibilities.
New variants require editing stable logicOCP introduces an extension point.
An implementation breaks expectations of an abstractionLSP restores substitutability.
Consumers depend on unrelated operationsISP narrows the contracts.
Business policy imports infrastructure detailsDIP reverses the dependency.

These principles reinforce one another. OCP commonly relies on an abstraction; LSP ensures that implementations honor it; ISP keeps it focused; DIP places it at the correct architectural boundary; and SRP keeps the participating modules cohesive.

Applying SOLID Carefully

SOLID principles are diagnostic tools, not mechanical rules. Applying them too early or too literally can produce excessive interfaces, indirection, and small classes that obscure the actual behavior of the program.

Use them in response to concrete design pressure:

  • Which changes currently affect too many modules?
  • Which classes serve unrelated actors?
  • Which conditionals repeatedly grow when variants are added?
  • Which subtype surprises code written against its base type?
  • Which clients depend on operations they never use?
  • Which business rules are coupled to volatile technical details?

The goal is not to maximize the number of abstractions. It is to arrange code so that likely changes remain local, contracts remain dependable, and important policy is protected from replaceable details.

References

  1. Robert C. Martin. Agile Software Development: Principles, Patterns, and Practices. Pearson, 2002.
  2. Robert C. Martin. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Pearson, 2017.
  3. Barbara Liskov and Jeannette M. Wing. “A Behavioral Notion of Subtyping.” ACM Transactions on Programming Languages and Systems, 1994.

zhuang@linux:~/notes/software-design/ood-principles-solid/$ comments