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:
- Its own object (
self) - Its parameters
- Objects it creates
- 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.