All books

A Philosophy of Software Design

Bookshelf

A Philosophy of Software Design

The Complexity Problem

  • Core problem: Recognize that complexity is the greatest impediment to software development
  • Complexity definition: Make note that it’s anything that makes software hard to understand or modify
  • Symptoms of complexity: Watch for change amplification, cognitive load, and unknown unknowns
  • Strategic vs. tactical programming: Choose strategic (long-term) over tactical (quick-fix) approach
  • Design principles: Focus on managing complexity as the fundamental goal of software design
  • Working code isn’t enough: Remember that how you write code matters as much as whether it works
  • Key insight: Invest time upfront in good design to save much more time later

The Nature of Complexity

  • Definition: Identify complexity as anything that makes software hard to understand or modify
  • Causes of complexity:
    • Eliminate dependencies between code elements
    • Reduce obscurity and unclear code
  • Complexity symptoms:
    • Change amplification: When a small change requires modifications in many places
    • Cognitive load: Mental effort needed to complete a task
    • Unknown unknowns: Unclear what needs to be modified or how
  • Incremental nature: Recognize that complexity builds up in small increments over time
  • Prevention approach: Take action on complexity as soon as you notice it
  • Measurement challenge: Acknowledge that complexity is subjective but still recognizable

Working Code Isn’t Enough

  • Beyond functionality: Create code that’s simple, obvious, and maintainable
  • Technical debt: Avoid taking shortcuts that will cost more to fix later
  • Strategic programming: Invest time upfront to produce clean, well-designed code
  • Continuous design: Improve design with each modification
  • Incremental investment: Make small improvements regularly rather than massive rewrites
  • Learning opportunity: Use each project to become a better programmer
  • Cost-benefit mindset: Remember that good design ultimately saves more time than it costs

Modules Should Be Deep

  • Module definition: Think of a module as any unit of code with an interface (functions, classes, etc.)
  • Interface vs. implementation: Separate what a module does from how it does it
  • Deep modules: Create modules with powerful functionality but simple interfaces
  • Shallow modules: Avoid modules whose interface complexity is similar to implementation complexity
  • Abstraction quality: Judge abstractions by how much functionality they provide and how simple their interface is
  • Information hiding: Conceal implementation details to allow independent modification
  • Unix philosophy misapplication: Don’t create dozens of tiny, shallow modules

Information Hiding and Leakage

  • Information hiding principle: Encapsulate design decisions that are likely to change
  • Benefits: Design for independent modification of components
  • Implementation details: Keep them private and flexible where possible
  • Information leakage: Prevent implementation details from being exposed in APIs
  • Temporal decomposition: Avoid designing classes around order of operations
  • Leakage detection: Watch for situations where changing one module requires changes to others
  • Pass-through methods: Eliminate methods that just pass calls to another object
  • Example red flags: Classes named after actions rather than entities, excessive getters/setters

General-Purpose Modules are Deeper

  • Generality principle: Design modules for broad, not narrow, use cases
  • Over-specialization: Avoid creating different classes for slightly different behaviors
  • Configuration parameters: Use them to make modules more flexible without complicating interfaces
  • Goldilocks rule: Make modules slightly more general than their current use requires
  • Example: Design a file class that handles different file types rather than a specialized class per type
  • Default values: Provide sensible defaults for configuration parameters
  • Warning: Don’t over-generalize when future needs are highly uncertain

Different Layer, Different Abstraction

  • Abstraction layers: Make each layer represent a distinct abstraction
  • Layer violation: Avoid exposing lower-level details in higher-level abstractions
  • Pass-through methods: Eliminate methods that merely forward to another class
  • Class hierarchy design: Create meaningful differences between parent and child classes
  • Decoration pattern overuse: Watch for decorators that don’t add real value
  • Interface duplication: Don’t repeat the same interface across layers
  • Dispatching: Consider adding a new layer rather than cluttering existing ones with dispatch code
  • Red flag: Beware when same abstractions appear at multiple layers

Pull Complexity Downwards

  • Complexity placement: Push complexity down into lower-level modules, not up to callers
  • User convenience: Make APIs simple at the expense of implementation complexity
  • Default values: Set sensible defaults rather than requiring configuration
  • Special case handling: Handle edge cases internally instead of forcing callers to handle them
  • Interface design: Judge interfaces by how easy they are to use, not how easy they are to implement
  • Implementation burden: Take on complexity in implementation to simplify interfaces
  • Example: Make a string class handle empty strings gracefully instead of requiring callers to check

Better Together or Better Apart?

  • Separation decision: Determine whether to divide or combine pieces of code
  • Bring together if:
    • They share information
    • They’re used together
    • They overlap conceptually
    • It’s hard to understand one without the other
  • Separate if:
    • They’re unrelated
    • It simplifies the interface
    • It reduces dependencies
  • General rule: Err on the side of bringing together for new systems
  • Subsystem design: Make subsystems self-contained with minimal external dependencies
  • Function length: Judge functions by clarity and abstraction level, not by lines of code
  • Comment hint: If you need comments to separate regions in a method, consider extracting those regions

Define Errors Out of Existence

  • Exception minimization: Design interfaces to eliminate exceptional conditions
  • Disguising exceptions: Handle special cases automatically when possible
  • Exception reduction: Reduce the number of places where errors must be handled
  • Crash-only software: Design systems to recover automatically after crashes
  • Mask exceptions: Turn exceptions into normal cases (e.g., creating a file if it doesn’t exist)
  • Exception aggregation: Handle many exceptions with a single piece of code
  • Just crash: For truly exceptional conditions, don’t try to recover in complex ways

Design it Twice

  • Multiple designs: Create at least two different designs for complex problems
  • Contrasting approaches: Explore fundamentally different approaches, not variations
  • Design comparison: Evaluate strengths and weaknesses of each approach
  • Early exploration: Do this before investing heavily in any implementation
  • Major operations: Focus your comparison on how each design handles common operations
  • Feature list: Compare designs against a specific list of features and requirements
  • Design documentation: Document the alternatives considered and the reasoning for choices

Why Write Comments? The Four Excuses

  • Value of comments: Use comments to capture information not obvious from the code
  • Combat excuses:
    • “Good code is self-documenting” – Not entirely true; code can’t explain why
    • “I don’t have time” – Comments save time over the life of the project
    • “Comments get out of date” – Keep them close to code they describe
    • “Comments are worthless” – Only bad comments are worthless
  • Comments’ purpose: Reduce cognitive load and improve maintainability
  • Design documentation: Record major design decisions and their rationales
  • Future investment: Write comments as an investment in future productivity

Comments Should Describe Things that Aren’t Obvious from the Code

  • Comment content: Explain things that aren’t obvious from the code alone
  • Avoid redundancy: Don’t restate what’s already clear from the code
  • Higher-level information: Document design decisions, rationales, and trade-offs
  • Abstractions: Explain what the abstraction represents in the problem domain
  • Preconditions: Document required states before method calls
  • Postconditions: Document guaranteed states after method returns
  • Interface comments: Focus on what the module does, not how
  • Implementation comments: Explain tricky, non-obvious code segments
  • Comment placement: Put comments where they’ll be seen when needed

Choosing Names

  • Name clarity: Pick names that reflect what the thing represents
  • Name precision: Choose names that are precise and unambiguous
  • Name consistency: Use similar naming patterns for similar things
  • Naming process: Iterate on names until they’re clear and descriptive
  • Method names: Include both what it does and what it returns
  • Avoid abbreviations: Prefer clarity over brevity (except for standard abbreviations)
  • Name length: Make it proportional to scope (smaller scope = shorter name)
  • Connotations: Consider what a name implies or suggests
  • Implementation details: Keep them out of interface names

Write The Comments First (Use Comments as Part of the Design Process)

  • Upfront commenting: Write interface comments before implementation
  • Design thinking: Use comment writing to clarify your design thoughts
  • Red flags: If a method is hard to describe, it’s probably a poor abstraction
  • Comment discipline: Comments first encourages better design and consistent documentation
  • Interface clarity: Clear comments lead to clearer interfaces
  • Implementation guidance: Well-written comments guide implementation
  • Documentation drift prevention: Update comments during implementation if design changes
  • Example workflow: Write class comment, then method comments, then implementation

Modifying Existing Code

  • Strategic approach: Maintain and improve design with each modification
  • Cognitive load: Minimize it for the next developer
  • Consistency: Keep stylistic consistency with surrounding code
  • Improvement opportunities: Look for chances to improve design during modifications
  • Tactical traps: Avoid quick hacks that worsen design
  • Comments: Update them with code changes
  • Boy Scout rule: Leave code cleaner than you found it
  • Legacy improvement: Gradually improve legacy systems through disciplined modifications

Consistency

  • Consistency importance: Write code that matches patterns and conventions
  • Areas for consistency:
    • Names
    • Coding style
    • Interfaces
    • Design patterns
    • Invariants
  • Documentation: Make deviations from consistency obvious and documented
  • Team standards: Establish and follow team consistency guidelines
  • Tools: Use automated formatting and linting tools to enforce consistency
  • Benefits: Consistent code is easier to read, understand, and modify
  • Adaptability: Allow consistency rules to evolve as needed

Code Should be Obvious

  • Obviousness principle: Write code whose behavior is obvious to a new reader
  • Cognitive load reduction: Make code predictable and follow conventions
  • Obscurity causes:
    • Event-driven programming
    • Generic containers
    • Different meanings for the same variable
    • Inconsistency
  • External knowledge: Minimize required external context to understand code
  • Surprising behavior: Document any code with non-obvious behavior
  • Readers’ perspective: Consider how your code looks to someone unfamiliar with it
  • Test of obviousness: Can someone understand your code after a quick skim?
  • Agile development: Focus on delivering working software but don’t neglect design
  • Test-driven development: Use tests to improve design, not just verify correctness
  • Design patterns: Use them to communicate, but don’t force them where they don’t fit
  • Object-oriented programming: Use it for its abstraction benefits, not dogmatically
  • Functional programming: Recognize its benefits for managing state
  • Inheritance overuse: Prefer composition in many cases
  • Microservices: Consider the complexity trade-offs versus monolithic designs
  • Trend evaluation: Judge trends by how they help manage complexity

Designing for Performance

  • Design vs. performance: Optimize designs for simplicity first, performance second
  • Measurement before optimization: Profile before making “optimizations”
  • Simplicity and performance: Often, simpler designs perform better
  • Performance-driven design: Redesign when performance issues are fundamental
  • Common bottlenecks: Look for unnecessary object creation and data copying
  • System boundaries: Watch for performance issues at API boundaries
  • Optimization impact: Consider how optimizations affect code clarity
  • End-to-end principle: Optimize complete operations, not individual components

Conclusion

  • Complexity reduction: Make it your primary design goal
  • Deep modules: Create modules with simple interfaces but powerful functionality
  • Information hiding: Conceal implementation details that are likely to change
  • General-purpose interfaces: Design for flexibility and reuse
  • Layering: Maintain distinct abstraction levels in your system
  • Pull complexity downward: Simplify interfaces by handling complexity internally
  • Strategic programming: Invest in good design consistently over time
  • Continuous improvement: Apply these principles incrementally with each change
  • Experience building: Develop your design intuition through practice and reflection

Key Takeaways

  1. Complexity is the enemy: Design software primarily to reduce complexity
  2. Deep modules: Create components with simple interfaces but powerful functionality
  3. Strategic programming: Invest time upfront to save more time later
  4. Information hiding: Conceal implementation details to allow independent modification
  5. Pull complexity down: Make interfaces simple even if it means more complex implementations
  6. Comments matter: Write them to explain things not obvious from the code
  7. Design it twice: Consider multiple approaches before implementing
  8. Consistency: Maintain consistent patterns and conventions
  9. Incremental improvement: Apply good design principles with every code change
  10. Obviousness: Write code whose behavior is clear to new readers