4 min read
On this page

Design Principles

Design principles guide the creation of maintainable, extensible, and correct software. They represent decades of collective wisdom from the software engineering community.

SOLID Principles

Single Responsibility Principle (SRP)

A class/module should have one reason to change — one responsibility.

// BAD: User handles persistence AND validation AND formatting
CLASS User
    PROCEDURE SAVE_TO_DB()       // persistence
    FUNCTION VALIDATE_EMAIL()     // validation
    FUNCTION TO_JSON()            // formatting

// GOOD: Separate responsibilities
CLASS User { name, email }
CLASS UserRepository  // save, find, delete
CLASS UserValidator    // validate fields
CLASS UserSerializer   // to_json, from_json

Benefit: Changes to validation don't affect persistence code. Easier to test. Clearer ownership.

Open-Closed Principle (OCP)

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

Add new behavior by adding new code, not changing existing code.

// GOOD: New shapes don't require modifying existing code
INTERFACE Shape
    FUNCTION AREA() -> number

CLASS Circle IMPLEMENTS Shape
    FUNCTION AREA()  RETURN PI * self.radius^2

CLASS Rectangle IMPLEMENTS Shape
    FUNCTION AREA()  RETURN self.w * self.h

// Adding Triangle: just implement Shape. No existing code changes.
FUNCTION TOTAL_AREA(shapes)
    RETURN SUM(AREA(s) FOR EACH s IN shapes)

Mechanism: Abstractions (traits/interfaces). Depend on abstractions, not concrete types.

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering program correctness.

If S is a subtype of T, then objects of type T can be replaced with objects of type S
without breaking the program.

Classic violation: Square extending Rectangle. If set_width and set_height are independent operations for Rectangle but coupled for Square, substituting a Square for a Rectangle breaks code that expects independent width/height changes.

Rule of thumb: If the subtype surprises users of the base type, it violates LSP.

Interface Segregation Principle (ISP)

Clients should not depend on interfaces they don't use. Prefer many small interfaces over one large one.

// BAD: One big interface
INTERFACE Worker
    PROCEDURE WORK()
    PROCEDURE EAT()
    PROCEDURE SLEEP()
// A Robot "worker" doesn't eat or sleep!

// GOOD: Segregated interfaces
INTERFACE Workable    PROCEDURE WORK()
INTERFACE Eatable     PROCEDURE EAT()
INTERFACE Sleepable   PROCEDURE SLEEP()

CLASS Human IMPLEMENTS Workable, Eatable
CLASS Robot IMPLEMENTS Workable
// Robot doesn't implement Eatable — no forced empty implementation

Dependency Inversion Principle (DIP)

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

// BAD: Business logic depends on a specific database
CLASS OrderService
    db: PostgresDB              // concrete dependency

// GOOD: Depend on an abstraction
INTERFACE OrderRepository
    FUNCTION SAVE(order) -> Result
    FUNCTION FIND(id) -> Order or NIL

CLASS OrderService
    repo: OrderRepository       // depends on abstraction
// Can use PostgresRepository, InMemoryRepository, MockRepository

Benefit: Testability (mock the repository). Flexibility (swap implementations). Decoupling.

Other Principles

DRY (Don't Repeat Yourself)

Every piece of knowledge should have a single, authoritative representation. Duplication leads to inconsistency.

Caveat: Don't over-DRY. Three similar lines of code are often better than a premature abstraction. Wait until you see the pattern three times before extracting.

KISS (Keep It Simple, Stupid)

The simplest solution that works is usually the best. Complexity is the enemy of reliability.

YAGNI (You Ain't Gonna Need It)

Don't build features "just in case." Build what you need now. Add complexity when it's justified by actual requirements.

Separation of Concerns

Divide a system into distinct sections, each addressing a separate concern (UI, business logic, data access, infrastructure).

Layered architecture: Presentation → Business Logic → Data Access → Database.

Law of Demeter (Principle of Least Knowledge)

A method should only call methods on:

  1. Its own object (self)
  2. Its parameters
  3. Objects it creates
  4. Its direct fields
// BAD: Train wreck — reaching through multiple objects
order.GET_CUSTOMER().GET_ADDRESS().GET_CITY()

// GOOD: Ask, don't tell. Delegate.
order.SHIPPING_CITY()

Composition Over Inheritance

Prefer composing objects (has-a) over inheriting behavior (is-a).

// Composition: Logger has a Formatter and Writer
CLASS Logger
    formatter: Formatter     // interface
    writer: Writer           // interface

// Much more flexible than:
// ConsoleLogger extends Logger
// FileLogger extends Logger
// JsonConsoleLogger extends ConsoleLogger
// JsonFileLogger extends FileLogger  <- explosion!

Program to Interfaces

Depend on abstractions (traits/interfaces), not concrete implementations.

FUNCTION PROCESS(data: Readable) -> Result
    // Works with files, network streams, in-memory buffers, ...
    buf ← READ_ALL(data)
    RETURN Ok(buf)

Dependency Injection

Instead of creating dependencies internally, receive them from outside (constructor, method parameter, framework).

// Constructor injection
CLASS UserService
    repo: UserRepository     // interface
    mailer: Mailer           // interface

    CONSTRUCTOR NEW(repo, mailer)
        self.repo ← repo
        self.mailer ← mailer

Cohesion and Coupling

High cohesion: Elements within a module are closely related and work toward a single purpose. Good.

Low coupling: Modules are independent and interact through narrow, well-defined interfaces. Good.

Goal: High cohesion within modules. Low coupling between modules.

Tell, Don't Ask

Instead of asking for data and making decisions, tell objects what to do.

// BAD: Ask for data, decide externally
IF account.BALANCE() ≥ amount
    account.SET_BALANCE(account.BALANCE() - amount)

// GOOD: Tell the object what to do
account.WITHDRAW(amount)   // object decides internally

Applying Principles

When Principles Conflict

Principles sometimes conflict:

  • DRY may violate KISS (abstraction adds complexity).
  • OCP may violate YAGNI (extension points for unused features).
  • SRP may create too many small classes.

Resolution: Principles are guidelines, not laws. Apply judgment. Optimize for readability and maintainability. Refactor when pain is felt, not in advance.

Code Smells (Indicators of Principle Violations)

| Smell | Likely Violated Principle | |---|---| | God class (does everything) | SRP | | Shotgun surgery (change touches many files) | SRP, Coupling | | Feature envy (method uses another class's data) | Law of Demeter, Tell Don't Ask | | Duplicate code | DRY | | Long method | SRP, readability | | Primitive obsession | Abstraction, domain modeling | | Inappropriate intimacy | Coupling, encapsulation |

Applications in CS

  • API design: SOLID principles produce APIs that are stable, extensible, and testable.
  • Library design: OCP and DIP enable extensibility without breaking existing users.
  • Microservices: Each service has single responsibility. Services communicate through abstractions (API contracts).
  • Testing: DIP enables mocking. SRP means units are small and testable.
  • Refactoring: Principles guide which refactorings to apply when cleaning up code.