Software Design
Software design is the discipline of organizing code so that it is correct, understandable, and changeable. A well-designed system makes the right things easy and the wrong things hard. This skill catalogs the principles, patterns, and architectural styles that professional software engineers use to achieve this, with emphasis on when each applies and when it does not.
Agent affinity: dijkstra (structured programming, formal reasoning about design), kay (OOP, message-passing architecture)
Concept IDs: code-code-organization, code-abstraction, code-decomposition, code-peer-review
Part 1 -- Design Principles
The SOLID Principles
Robert C. Martin codified these five principles for object-oriented design. They apply more broadly to any modular system.
S -- Single Responsibility Principle (SRP)
Statement: A module should have one, and only one, reason to change.
What it means. Each class, function, or module should serve exactly one purpose. If you change the database schema, only the database layer should change. If you change the UI layout, only the UI layer should change.
Violation signal. A class that has methods for both parsing user input AND writing to the database. A function that both validates data AND sends an email.
Fix. Separate the responsibilities into distinct modules. A UserValidator validates; a UserRepository persists; an EmailService sends.
O -- Open/Closed Principle (OCP)
Statement: Software entities should be open for extension but closed for modification.
What it means. You should be able to add new behavior without changing existing, tested code. This is achieved through abstraction -- interfaces, abstract classes, or polymorphism.
Violation signal. A function with a growing chain of if/else or switch statements that must be modified every time a new case is added.
Fix. Define an interface that each case implements. New cases add a new implementation without touching existing code. The Strategy pattern is the canonical implementation of OCP.
L -- Liskov Substitution Principle (LSP)
Statement: Objects of a supertype should be replaceable with objects of a subtype without breaking the program.
What it means. Subclasses must honor the contract of their parent. If a function works with a Shape, it must work with Circle, Rectangle, and Triangle without knowing which one it has.
The classic violation. Square extends Rectangle. A function sets width to 5 and height to 10, then asserts area == 50. The Square overrides setWidth to also set height, so area == 100. The substitution broke the contract.
Fix. Do not make Square a subclass of Rectangle. Model them as siblings implementing a common Shape interface, or use composition instead of inheritance.
I -- Interface Segregation Principle (ISP)
Statement: No client should be forced to depend on methods it does not use.
What it means. Large interfaces should be split into smaller, more specific ones. A Printer interface with print(), scan(), fax(), and staple() forces a simple printer to implement fax() and staple() with stubs or exceptions.
Fix. Split into Printable, Scannable, Faxable. Clients depend only on the interfaces they use. Composition of small interfaces is more flexible than one large interface.
D -- Dependency Inversion Principle (DIP)
Statement: High-level modules should not depend on low-level modules. Both should depend on abstractions.
What it means. The business logic should not import the database driver directly. Instead, both the business logic and the database implementation depend on a Repository interface. This makes the database swappable without changing the business logic.
Implementation. Dependency injection: pass dependencies as constructor parameters or function arguments rather than creating them internally. This also enables testing -- inject a mock repository in tests, a real one in production.
Complementary Heuristics
DRY (Don't Repeat Yourself). Every piece of knowledge should have a single, unambiguous, authoritative representation. Duplication is a maintenance liability. But premature de-duplication (abstracting too early) can be worse than duplication -- wait until you see the pattern three times (the Rule of Three).
KISS (Keep It Simple, Stupid). The simplest solution that works is the best solution. Complexity is a cost, not a feature. Every abstraction layer, design pattern, and framework adds complexity that must be justified by the problems it solves.
YAGNI (You Aren't Gonna Need It). Do not build features, abstractions, or flexibility for hypothetical future requirements. Build what you need now. When the future requirement arrives, you will understand it better and build a more appropriate solution.
Separation of Concerns. Each module addresses a separate concern. The UI does not contain business logic. The business logic does not contain database queries. The database layer does not format HTTP responses. This is the meta-principle behind SOLID, MVC, and layered architecture.
Coupling and Cohesion
Coupling measures how much modules depend on each other. Low coupling means modules can be changed independently. High coupling means a change in one module cascades through many others.
Cohesion measures how closely related the elements within a module are. High cohesion means a module does one thing well. Low cohesion means a module is a grab-bag of unrelated functionality.
The goal: high cohesion within modules, low coupling between modules. This is the quantitative formulation of good design.
Part 2 -- Design Patterns
Design patterns are reusable solutions to common design problems. They are not code templates -- they are conceptual tools for structuring interactions between objects and modules. The Gang of Four (GoF) cataloged 23 patterns in 1994; the 12 most practically important are covered here.
Creational Patterns
Factory Method
Problem: Client code needs to create objects but should not know the concrete class.
Solution: Define a method that returns an instance of an interface. Subclasses or configuration determine the concrete class.
When to use. When the creation logic is complex, when the concrete class depends on runtime conditions, or when you want to decouple client code from implementation classes.
Builder
Problem: Constructing complex objects with many optional parameters leads to telescoping constructors or error-prone parameter ordering.
Solution: Separate the construction of a complex object from its representation. The builder accumulates configuration and produces the final object in a build() call.
When to use. Objects with more than 3-4 optional parameters. Immutable objects that require all fields at construction time. Configuration objects.
Singleton
Problem: Ensure a class has exactly one instance and provide a global access point.
Solution: Private constructor, static instance method.
When to use. Almost never. Singleton is the most overused pattern. It creates hidden global state, makes testing difficult, and couples all consumers to the singleton. Prefer dependency injection. Use singleton only for truly global, stateless services (logging, metrics) and even then consider alternatives.
Structural Patterns
Adapter
Problem: Two interfaces are incompatible but need to work together.
Solution: Create a wrapper that translates calls from one interface to the other.
When to use. Integrating third-party libraries, wrapping legacy APIs, bridging between different data formats.
Decorator
Problem: Add behavior to an object dynamically without modifying its class.
Solution: Wrap the object in a decorator that implements the same interface and adds behavior before or after delegating to the wrapped object.
**When to use