Reading notes: A Philosophy of Software Design, 2nd Edition


~11 minutes to read
Note: This article features skimming notes and is intentionally less fluffy.

Table of contents


Nature of complexity

  • "Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places." (e.g., refactoring a project to inject dependencies in tests, because the system was hard to test in the first place)
  • "Cognitive load: The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task." (e.g., close a file where it is opened whenever possible, instead of leaking context and pushing the responsibility onto the caller)
  • "Unknown unknowns: The third symptom of complexity is that it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully."
  • "One of the most important goals of good design is for a system to be obvious."

Strategic vs. tactical programming

  • "it's worth taking a little extra time to find a simple design for each new class; rather than implementing the first idea that comes to mind, try a couple of alternative designs and pick the cleanest one. Try to imagine a few ways in which the system might need to be changed in the future and make sure that will be easy with your design"

Modules should be deep

  • Modular design
    • "In general, if a developer needs to know a particular piece of information in order to use a module, then that information is part of the module's interface."
  • Abstractions
    • "The key to designing abstractions is to understand what is important, and to look for designs that minimize the amount of information that is important"
  • A deep module is one in which the interface is small and the implementation is large.
    • "Deep modules such as Unix I/O and garbage collectors provide powerful abstractions because they are easy to use, yet they hide significant implementation complexity."
  • Shallow modules
    • "A shallow module is one whose interface is complicated relative to the functionality it provides."

Information hiding

  • "If a piece of information is hidden, there are no dependencies on that information outside the module containing the information, so a design change related to that information will affect only the one module"
  • Information leakage
    • "If the affected classes are relatively small and closely tied to the leaked information, it may make sense to merge them into a single class."
    • "When designing modules, focus on the knowledge that's needed to perform each task, not the order in which tasks occur."
    • "it's important to avoid exposing internal data structures as much as possible"
  • "The best features are the ones you get without even knowing they exist."
  • "As a software designer, your goal should be to minimize the amount of information needed outside a module; for example, if a module can automatically adjust its configuration, that is better than exposing configuration parameters"
  • "think about the different pieces of knowledge that are needed to carry out the tasks of your application, and design each module to encapsulate one or a few of those pieces of knowledge. This will produce a clean and simple design with deep modules."

General-Purpose Modules are Deeper

  • "specialization leads to complexity"
  • Generality leads to better information hiding
  • "Eliminate special cases in code"

Different Layer, Different Abstraction

  • "A pass-through method is one that does nothing except pass its arguments to another method, usually with the same API as the pass-through method. This typically indicates that there is not a clean division of responsibility between the classes."

Pull Complexity Downwards

  • Figure out complexity in the library instead of pushing it up to the user.
  • "[...] you should avoid configuration parameters as much as possible. Before exporting a configuration parameter, ask yourself: will users (or higher-level modules) be able to determine a better value than we can determine here?"

Better Together Or Better Apart?

  • "Bring together if information is shared"
  • "Bring together if it will simplify the interface"
  • "Bring together to eliminate duplication"
    • Use goto to exit nested code blocks early as long as it's not too complex.
  • On Splitting long methods: "If you make a split of this form and then find yourself flipping back and forth between the parent and child to understand how they work together, that is a red flag ("Conjoined Methods") indicating that the split was probably a bad idea."
  • "If the caller has to invoke each of the separate methods, passing state back and forth between them, then splitting is not a good idea."

Define Errors Out Of Existence

  • "The key overall lesson from this chapter is to reduce the number of places where exceptions must be handled; in some cases the semantics of operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report [...]."
  • "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"
  • E.g., the Java substring method could remove its IndexOutOfBoundsException by automatically rounding the index to the nearest valid value.
  • "Exception masking doesn't work in all situations, but it is a powerful tool in the situations where it works. It results in deeper classes, since it reduces the class's interface (fewer exceptions for users to be aware of) and adds functionality in the form of the code that masks the exception. Exception masking is an example of pulling complexity downward."
  • "The idea behind exception aggregation is to handle many exceptions with a single piece of code; rather than writing distinct handlers for many individual exceptions, handle them all in one place with a single handler." (e.g., a web server dispatching different types of requests and catching all sorts of errors).
  • "In most applications there will be certain errors that are not worth trying to handle. Typically, these errors are difficult or impossible to handle and don't occur very often. The simplest thing to do in response to these errors is to print diagnostic information and then abort the application." (e.g., out-of-memory errors).

Design it Twice

  • "The initial design experiments will probably result in a significantly better design, which will more than pay for the time spent designing it twice"
  • "The process of devising and comparing multiple approaches will teach you about the factors that make designs better or worse. Over time, this will make it easier for you to rule out bad designs and hone in on really great ones."

Why Write Comments? The Four Excuses

  • Good code is self-documenting. Comments serve as precisions on abstractions.
  • I don't have time to write comments. The benefits of having documentation quickly offset the cost of writing it.
  • Comments get out of date and become misleading. Keep the documentation close to where it needs to be, so you update it.
  • All the comments I have seen are worthless. Be the change you want to see.
  • Benefits of well-written comments
    • "The overall idea behind comments is to capture information that was in the mind of the designer but couldn't be represented in the code."
    • "One of the purposes of comments is to make it it unnecessary to read the code."

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

  • Don't repeat the code
  • "Lower-level comments add precision"
    • Focus on what the variable represents, not how it is modified (the latter is explained by the code).
  • "Higher-level comments enhance intuition"
    • "What is the simplest thing you can say that explains everything in the code? What is the most important thing about this code?"
  • Interface documentation
  • "Implementation comments: what and why, not how"
    • Describe at a more abstract level to help understand what is happening and why. The code tells the how.
  • Cross-module design decisions
    • One possible solution is to use a designNotes.md file or similar to jot down all major decisions, and point to it from the code (e.g., See topic in designNotes).

Choosing names

  • Great names are "precise, unambiguous, and intuitive."
  • Use names consistently: make it so that a concept is expressed in the same words regardless of the context in which it is found if it makes sense to do so.
  • Avoid extra words: no more Hungarian Notation with modern IDEs.

Use comments as part of the design process

  • Delayed comments are bad comments
    • You won't remember exactly what you had in mind and why you did things if you delay writing documentation: design decisions will get lost.
  • Write the comments first
    • No backlog of unwritten comments.
    • "it produces better comments."
  • Comments are a design tool
    • Writing them at the beginning "improves the system design."
    • "If a method or variable requires a long comment, it is a red flag that you don't have a good abstraction."
  • Early comments are fun comments
    • "If you are programming strategically, where your main goal is a great design rather than just writing code that works, then writing comments should be fun, since that's how you identify the best designs."
    • "I'm looking for the design that can be expressed completely and clearly in the fewest words. The simpler the comments, the better I feel about my design."
  • Are early comments expensive?
    • "Writing the comments first will mean that the abstractions will be more stable before you start writing code."
    • "In contrast, if you write the code first, the abstractions will probably evolve as you code, which will require more code revisions than the comments-first approach."

Modifying existing code

  • "Whenever you modify any code, try to find a way to improve the system design at least a little bit in the process. If you're not making the design better, you are probably making it worse."
  • Maintaining comments: keep the comments near the code
    • "The farther a comment is from its associated code, the less likely it is that it will be updated properly."
    • "In general, the farther a comment is from the code it describes, the more abstract it should be (this reduces the likelihood that the comment will be invalidated by code changes)."
  • Comments belong in the code, not the commit log
    • "An example is a commit message describing a subtle problem that motivated a code change. If this isn't documented in the code, then a developer might come along later and undo the change without realizing that they have re-created a bug."
  • Maintaining comments: avoid duplication
  • Maintaining comments: check the diffs
    • Review changes during the pre-commit phase to make sure documentation stays up-to-date with the code changes.
  • Higher-level comments are easier to maintain
    • Most helpful comments do not repeat the code, they provide an abstraction that can't be expressed easily by the code alone.

Consistency

  • Similar things are done in a similar way.
  • "Consistency creates cognitive leverage: once you have learned how something is done in one place, you can use that knowledge to immediately understand other places that use the same approach."
  • Ensuring consistency
    • Document most important conventions.
    • "the value of consistency over inconsistency is almost always greater than the value of one approach over another."

Code should be obvious

  • "the best way to determine the obviousness of code is through code reviews"
  • "If someone reading your code says it's not obvious, then it's not obvious, no matter how clear it may seem to you"
  • Things that make code more obvious
    • Good names
    • Consistency
    • Judicious use of white space
    • Blank lines
    • Comments
  • Things that make code less obvious
    • Event-driven programming
    • Generic containers
    • Different types for declaration and allocation
  • Object-oriented programming and inheritance
    • "In order for an interface to have many implementations, it must capture the essential features of all the underlying implementations while steering clear of the details that differ between the implementations; this notion is at the heart of abstraction."
    • "Class hierarchies that use implementation inheritance extensively tend to have high complexity."
    • "Before using implementation inheritance, consider whether an approach based on composition can provide the same benefits."
  • Agile development
    • May encourage a more tactical approach to programming. Balance is required.
  • Test-driven development
    • "it focuses attention on getting specific features working, rather than finding the best design"
    • "One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug."
  • Design patterns
    • "As with many ideas in software design, the notion that design patterns are good doesn’t necessarily mean that more design patterns are better."
  • Getters and setters
    • "It’s better to avoid getters and setters (or any exposure of implementation data) as much as possible."

Designing for performance

  • How to think about performance
    • "The key is to develop an awareness of which operations are fundamentally expensive."
  • Measure before (and after) modifying
    • "the goal is to identify a small number of very specific places where the system is currently spending a lot of time, and where you have ideas for improvement."
    • "If the changes didn’t make a measurable difference in performance, then back them out (unless they made the system simpler)."
  • Design around the critical path
    • "When redesigning for performance, try to minimize the number of special cases you must check."
  • "Complicated code tends to be slow because it does extraneous or redundant work."

Decide what matters

  • Minimize what matters
  • How to emphasize things that matter
    • "important things should appear in places where they are more likely to be seen, such as interface documentation, names, or parameters to heavily used methods."
    • "repetition: key ideas appear over and over again."
    • "The things that matter the most should be at the heart of the system, where they determine the structure of things around them."
  • Mistakes
    • "treat too many things as important. When this happens, unimportant things clutter up the design, adding complexity and increasing cognitive load."
    • "fail to recognize that something is important. This mistake leads to situations where important information is hidden, or important functionality is not available so developers must continually recreate it."
  • Thinking more broadly
    • "the best way to make a document easy to read is to identify a few key concepts at the beginning and structure the remainder of the document around them. When discussing the details of a system, it helps to tie them back to the overall concepts."
    • "Focusing on what is important is also a great life philosophy: identify a few things that matter most to you, and try to spend as much of your energy as possible on those things."

Conclusion

  • "The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code."