A Philosophy of Software Design, 2nd Edition receives mixed reviews. Many praise its insights on managing complexity and designing deep modules, while some criticize its emphasis on comments and lack of depth in certain areas. Readers appreciate the clear writing and practical advice, particularly for newer developers. However, some experienced programmers find it too basic or disagree with certain recommendations. The book's focus on object-oriented programming and academic perspective is noted, with some wishing for more diverse language examples and real-world applications.
Complexity is the root of software design challenges
Strategic programming trumps tactical approaches
Modules should be deep, not shallow
Good interfaces are the key to managing complexity
Comments are crucial for creating abstractions
Consistent naming and formatting enhance readability
Continuous refinement is essential for maintaining clean design
Error handling should be simplified, not proliferated
General-purpose code is usually better than special-purpose solutions
Write code for readability, not for ease of writing
Complexity comes from an accumulation of dependencies and obscurities.
Complexity accumulates incrementally. As software systems grow, they tend to become more complex due to the gradual accumulation of dependencies between components and obscure code sections. This complexity manifests in three primary ways:
Change amplification: Small changes require modifications in many places
Cognitive load: Developers need to understand large amounts of information to make changes
Unknown unknowns: It's unclear what code needs to be modified or what information is relevant
Simplicity is the antidote. To combat complexity, software designers should focus on creating simple, obvious designs that minimize dependencies and obscurities. This involves:
Modular design: Dividing systems into independent modules
Information hiding: Encapsulating implementation details within modules
Clear abstractions: Providing simple interfaces that hide underlying complexity
The best approach is to make lots of small investments on a continual basis.
Long-term thinking yields better results. Strategic programming focuses on creating a great design that happens to work, rather than just making code work. This approach involves:
Investing time in design upfront
Continually making small improvements
Refactoring code to maintain clean design
Tactical programming leads to technical debt. While tactical approaches may seem faster in the short term, they often result in:
Accumulation of quick fixes and hacks
Increasing difficulty in making changes over time
Higher long-term development costs
By adopting a strategic mindset, developers can create systems that are easier to maintain and evolve, ultimately saving time and effort in the long run.
The best modules are those that provide powerful functionality yet have simple interfaces.
Depth creates abstraction. Deep modules hide significant implementation complexity behind simple interfaces. This approach:
Reduces cognitive load for users of the module
Allows for easier modification of the implementation
Promotes information hiding and encapsulation
Shallow modules add complexity. Modules with complex interfaces relative to their functionality are considered shallow. These modules:
Increase the overall system complexity
Expose unnecessary implementation details
Make the system harder to understand and modify
To create deep modules, focus on designing simple, intuitive interfaces that abstract away the underlying complexity. Strive to maximize the ratio of functionality to interface complexity.
The interface to a module contains two kinds of information: formal and informal.
Well-designed interfaces simplify systems. Good interfaces provide a clear abstraction of a module's functionality without exposing unnecessary details. They should:
Be simple and intuitive to use
Hide implementation complexities
Provide both formal (e.g., method signatures) and informal (e.g., high-level behavior descriptions) information
Interfaces should evolve thoughtfully. When modifying existing code:
Consider the impact on the module's interface
Avoid exposing implementation details
Strive to maintain or improve the abstraction provided by the interface
By focusing on creating and maintaining good interfaces, developers can manage complexity and make their systems more modular and easier to understand.
Comments provide the only way to fully capture abstractions, and good abstractions are fundamental to good system design.
Comments complete abstractions. While code can express implementation details, comments are essential for capturing:
High-level design decisions
Rationale behind choices
Expectations and constraints
Abstractions that aren't obvious from the code alone
Write comments first. By writing comments before implementing code:
You clarify your thinking about the design
You can evaluate and refine abstractions early
You ensure documentation is always up-to-date
Focus on what and why, not how. Good comments should:
Describe things that aren't obvious from the code
Explain the purpose and high-level behavior of code
Avoid merely repeating what the code does
By prioritizing clear, informative comments, developers can create better abstractions and improve the overall design of their systems.
Good names are a form of documentation: they make code easier to understand.
Consistency reduces cognitive load. By establishing and following conventions for naming and formatting, developers can:
Make code more predictable and easier to read
Reduce the mental effort required to understand code
Highlight inconsistencies that may indicate bugs or design issues
Choose names carefully. Good names should:
Be precise and unambiguous
Create a clear image of the entity being named
Be used consistently throughout the codebase
Formatting matters. Consistent formatting helps by:
Making the structure of code more apparent
Grouping related elements visually
Emphasizing important information
By paying attention to naming and formatting, developers can significantly improve the readability and maintainability of their code.
If you want a clean software structure, which will allow you to work efficiently over the long-term, then you must take some extra time up front to create that structure.
Design is an ongoing process. Clean software design requires:
Regular refactoring to improve existing code
Continual evaluation of design decisions
Willingness to make changes as the system evolves
Invest in improvement. To maintain a clean design:
Allocate time for cleanup and refactoring
Address design issues promptly, before they compound
View each code change as an opportunity to improve the overall design
Balance perfection and progress. While striving for clean design:
Recognize that some compromises may be necessary
Focus on making incremental improvements
Prioritize changes that provide the most significant benefits
By treating design as a continuous process of refinement, developers can keep their systems clean and manageable as they grow and evolve.
The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence.
Reduce exception cases. To simplify error handling:
Design APIs to minimize exceptional conditions
Use default behaviors to handle common edge cases
Consider whether exceptions are truly necessary
Aggregate error handling. When exceptions are unavoidable:
Handle multiple exceptions in a single place when possible
Use exception hierarchies to simplify handling of related errors
Avoid catching exceptions you can't meaningfully handle
Make normal cases easy. Focus on making the common, error-free path through your code as simple and obvious as possible. This approach:
Reduces the cognitive load on developers
Minimizes the chances of introducing bugs
Makes the code easier to understand and maintain
By simplifying error handling, developers can create more robust and easier-to-understand systems.
Even if you use a class in a special-purpose way, it's less work to build it in a general-purpose way.
Generality promotes reusability. General-purpose code:
Can be applied to a wider range of problems
Is often simpler and more abstract
Tends to have cleaner interfaces
Avoid premature specialization. When designing new functionality:
Start with a somewhat general-purpose approach
Resist the urge to optimize for specific use cases too early
Allow the design to evolve based on actual usage patterns
Balance generality and simplicity. While striving for general-purpose solutions:
Avoid over-engineering or adding unnecessary complexity
Ensure the general-purpose design is still easy to use for common cases
Be willing to create specialized solutions when truly necessary
By favoring general-purpose designs, developers can create more flexible and maintainable systems that are better equipped to handle future requirements.
Software should be designed for ease of reading, not ease of writing.
Prioritize long-term maintainability. When writing code:
Focus on making it easy to understand for future readers
Avoid shortcuts or clever tricks that obscure the code's purpose
Invest time in creating clear abstractions and documentation
Make code obvious. Strive to write code that:
Can be understood quickly with minimal mental effort
Uses clear and consistent naming conventions
Has a logical and easy-to-follow structure
Refactor for clarity. Regularly review and improve existing code:
Look for opportunities to simplify complex sections
Break down long methods into smaller, more focused pieces
Eliminate duplication and inconsistencies
By prioritizing readability over ease of writing, developers can create systems that are easier to maintain, debug, and extend over time. This approach may require more effort initially but pays off in reduced long-term complexity and improved team productivity.