Design Principles

dry

don’t repeat yourself.

every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

when you find yourself copying code, extract it into a shared function, class, or module. duplication leads to:

  • inconsistent bug fixes (fix in one place, forget another)
  • increased maintenance burden
  • divergent behaviour over time

kiss

keep it simple, stupid.

the simplest solution that works is usually the best. complexity should be added only when necessary, not speculatively.

avoid:

  • premature optimisation
  • over-engineering for hypothetical future requirements
  • clever code that’s hard to read

encapsulate what varies

identify the aspects of your application that vary and separate them from what stays the same.

this is the foundation of many design patterns:

program to an interface, not an implementation

depend on abstractions rather than concrete classes.

// bad: coupled to ArrayList
ArrayList<String> items = new ArrayList<>();

// good: coupled to List interface
List<String> items = new ArrayList<>();

benefits:

  • swap implementations without changing client code
  • easier to mock in tests
  • reduces ripple effects of changes

favour composition over inheritance

inheritance (“is-a”) creates tight coupling between parent and child classes. composition (“has-a”) is more flexible.

InheritanceComposition
static, compile-timedynamic, runtime
tight couplingloose coupling
fragile base class problemno inheritance hierarchy to break
single inheritance (most langs)compose multiple behaviours freely

use inheritance when:

use composition when:

  • you need to reuse behaviour across unrelated classes
  • you want runtime flexibility
  • the relationship is “has-a” or “uses-a”

SOLID

five principles for object-oriented design, introduced by Robert C. Martin.

single responsibility principle

a class should have only one reason to change.

each class should do one thing and do it well. when a class has multiple responsibilities, changes to one responsibility risk breaking the others.

signs of violation

  • class name includes “And” or “Manager” or “Handler” (vague)
  • you modify the same class for unrelated features
  • class has many unrelated methods

example

// violates SRP: handles both persistence and formatting
class Employee {
    void saveToDatabase() { ... }
    String formatAsJson() { ... }
    void calculatePay() { ... }
}

// better: separate concerns
class Employee { void calculatePay() { ... } }
class EmployeeRepository { void save(Employee e) { ... } }
class EmployeeSerializer { String toJson(Employee e) { ... } }

open/closed principle

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

you should be able to add new behaviour without changing existing code. this is typically achieved through:

  • inheritance and polymorphism
  • composition and delegation
  • strategy pattern

example

// closed for modification: don't touch this
interface Shape { double area(); }

// open for extension: add new shapes freely
class Circle implements Shape {
    double radius;
    double area() { return Math.PI * radius * radius; }
}

class Rectangle implements Shape {
    double width, height;
    double area() { return width * height; }
}

Liskov substitution principle

subtypes must be substitutable for their base types.

if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

rules

  • preconditions cannot be strengthened in a subtype
  • postconditions cannot be weakened in a subtype
  • invariants of the supertype must be preserved
  • the “history constraint”: subtypes should not introduce methods that mutate state in ways the supertype doesn’t allow

classic violation

class Rectangle {
    void setWidth(int w) { this.width = w; }
    void setHeight(int h) { this.height = h; }
}

class Square extends Rectangle {
    // violates LSP: can't independently set width/height
    void setWidth(int w) { this.width = w; this.height = w; }
    void setHeight(int h) { this.width = h; this.height = h; }
}

client code expecting Rectangle behaviour will break when given a Square.

interface segregation principle

clients should not be forced to depend on interfaces they do not use.

prefer many small, specific interfaces over one large, general-purpose interface.

example

// bad: fat interface
interface Worker {
    void work();
    void eat();
    void sleep();
}

// good: segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }

class Robot implements Workable {
    void work() { ... }
    // doesn't need eat() or sleep()
}

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.

example

// bad: high-level depends on low-level
class OrderService {
    private MySQLDatabase db = new MySQLDatabase();
    void save(Order o) { db.insert(o); }
}

// good: both depend on abstraction
interface Database { void insert(Object o); }

class OrderService {
    private Database db;
    OrderService(Database db) { this.db = db; }
    void save(Order o) { db.insert(o); }
}

class MySQLDatabase implements Database { ... }
class PostgresDatabase implements Database { ... }

law of demeter

also known as the “principle of least knowledge”.

a method should only call methods of:

  • itself (this)
  • its own fields
  • its parameters
  • objects it creates locally

violation

// bad: message chain, knows too much about structure
customer.getWallet().getMoney().pay(amount);

// good: tell, don't ask
customer.pay(amount);

the first version couples the caller to the internal structure of Customer, Wallet, and Money. if any of those change, this code breaks.

coupling and cohesion

strive for low coupling and high cohesion.

coupling
the degree to which one module depends on another. low coupling means modules can change independently.
cohesion
the degree to which elements within a module belong together. high cohesion means a module does one thing well.
Low CouplingHigh Cohesion
modules are independentrelated code is grouped together
changes don’t ripplesingle, clear purpose
easier to test in isolationeasier to understand
can be reused in other contextsless internal fragmentation

design by contract

a methodology where software designers define formal, precise and verifiable interface specifications for software components.

precondition
what must be true before a method is called (caller’s responsibility)
postcondition
what will be true after a method returns (callee’s guarantee)
invariant
what must always be true for an object (class’s guarantee)

this is the other side of the coin to defensive programming, where the callee checks everything.

/**
 * @pre amount > 0
 * @pre balance >= amount
 * @post balance == old(balance) - amount
 */
void withdraw(int amount) {
    assert amount > 0 : "amount must be positive";
    assert balance >= amount : "insufficient funds";
    balance -= amount;
}

class invariants

a class invariant is a property or condition that must always hold true for all valid instances of a class, before and after any public method call.

examples

  • Stack: size > 0= and size < capacity=
  • BankAccount: balance > 0= (or minimum allowed)
  • Rectangle: width > 0 and height > 0
  • SortedList: elements are always in non-decreasing order

invariants are enforced by:

  • constructors (establish the invariant)
  • methods (preserve the invariant)
  • encapsulation (prevent external code from breaking it)

yagni

you aren’t gonna need it.

don’t implement functionality until you actually need it. speculative generality is a code smell.

  • build for today’s requirements, not tomorrow’s guesses
  • simpler code is easier to change when requirements do evolve
  • unused abstractions add cognitive load