Book Review: A Philosophy of Software Design


TLDR

Rating: 4 out of 5.

This book is a great read and quite remarkable in its brevity. 

The author made multiple insightful points; I found myself nodding vigorously in agreement with the numerous insightful gems spread across the book.

Ultimately, the book’s core thesis is minimizing complexity in software development by adopting complexity-eliminating approaches. The upfront investment in learning and adopting better designs pays off because it leads to high-quality software.

Things I like about the book

  • Short and sweet: Concise with little cruft.
  • Clarity: Expressive terminology, examples, and diagrams for succinctly conveying complex concepts; lucid diagrams simplified the explanation of complicated sections like pass-through methods and shared variables.

Things that could be better

  • More examples: having a wide variety of real-life examples from multiple projects would have helped.
  • Broader scope: The book does not discuss other aspects of software development: testing, reviews, release agility, etc.

Who should read this book?

I recommend this book to software engineers and line managers because of its tactical focus and valuable advice. I do not think upper-level managers would find it very useful since they are usually farther away from low-level details.

Get the book from Amazon here.

Chapter Notes

Chapter 4: Modules Should Be Deep

  • The best modules have interfaces that are simpler than their implementations.
  • The safest interfaces are fully explicit; module specifications should contain all the information a developer needs. Implicit interface aspects lead to tribal knowledge, especially since these esoteric details can’t be checked by tools, compilers, or programming languages.
  • Prioritize the most common use cases during design and provide specialized handling for non-common scenarios.

Chapter 7: Different Layer, Different Abstraction

  • A pass-through method does nothing except pass its arguments to another method. Having multiple functions with the same signature typically indicates a design smell (there is not a clean separation of responsibility between classes).
    • Notes: This resonated because I have committed this design flaw in the past; pass-through methods are challenging to debug and understand.
  • The interface to some piece of functionality should be in the same class that implements the functionality.
  • Pass-through variables are tougher to handle – using a shared context is a great way to avoid this.

Chapter 10: Define errors out of existence

  • Exceptions breed more exceptions; for example, retries can necessitate more code to handle new conditions, e.g., thundering herds, duplicate messages, and rate-limiting. It is also tricky to replicate certain exceptions in test environments, e.g., IOExceptions.
  • Throwing exceptions is easy; handling them is hard. If you are having difficulty figuring out how to handle a case, punting that decision to callers is also suboptimal since they probably will find it doubly tricky too.
  • Exceptions lead to complex interfaces; some argue that defining errors out of existence will miss out on catching bugs; the counterargument is that handling exceptions necessitates writing more code to handle such exceptions. Given the complexity of software, less is more, simple > complex. More code, more bugs. 
  • I like RAMCloud’s approach of crashing a server whenever a corrupt object is discovered. This technique significantly increases the recovery cost; however, simplification and frequent code execution benefits make up for that cost.

Chapter 11: Design it Twice

  • Having at least two designs before implementation leads to better architecture – allows for objectively comparing and contrasting approaches.
  • Maintaining well-designed software is cheaper than reworking poor architectural choices.

Chapter 12: Why Write Comments? The Four Excuses

  • I like his approach of estimating how much time engineers spend writing code (versus designing, reviewing, compiling, testing, etc.) and using that to set an upper bound for writing documentation.
  • Overall, this chapter felt light on detailed examples.

Chapter 13: Comments Should Describe Things that aren’t obvious from the code

  • Cross-module documentation wherein updating a single file (e.g., adding a new status code) leads to multiple changes across multiple files. The suboptimal but widespread solution for this ubiquitous challenge is to consult the experts and leverage their tribal knowledge. I love his recommendation of using comments to signal what files to change; this empowers the entire team and reduces cognitive overload.
  • Don’t Repeat Yourself (DRY) applies to documentation, especially for scenarios involving disparate files. Consider creating a central file to hold all the details and referring to that file from consuming locations.
  • I can’t entirely agree with the pedantic approach to over-commenting everything. Comments are not an end to themselves; instead, they serve to cognitive overload. 
  • Examples from RAMCloud dominated the chapter; a variety of samples would have been more useful.

Chapter 14: Choosing Names

  • Poor naming compounds negatively since software projects have thousands of variable, method, and class names. A single lousy name has a limited impact; however, many bad labels will lead to obscurity and increase complexity.
  • Names are abstractions; thus, great names are precise, unambiguous, and intuitive. 
  • If you struggle to name a variable, that might indicate underlying design issues.
  • Consistency in naming: if it looks like a duck, swims like a duck, and quacks like a duck, then it better be is a duck.

Chapter 15: Write The Comments First

  • Putting off documentation as the last thing in the development process leads to poor quality docs; frontload it and do it first.
  • Struggling to document a method or class might indicate poor design.

Chapter 16: Modifying Existing Code

  • If you are not making the overall system design better with each change, you are probably making it worse. Entropy is the default state of systems.
  • I disagree that keeping comments next to code mitigates the code rot problem fully. It ameliorates it but does not solve it significantly.
  • I like the heuristic: the farther away a comment block is from its implementation, the more abstract it should be
  • DRY applies to comments and documentation too! The author breaks the rule by regurgitating some past work in this section.

Chapter 17: Consistency

  • Consistency reduces cognitive overload; it creates mental leverage because developers can pattern-match. Naming and coding conventions help (as well as a healthy dose of nit-picky code reviews and documenting team engineering expectations).
  • Rarely introduce new styles – the disruptive cost of losing consistency rarely justifies the price. Stick with established styles even if they are not the most perfect.

Chapter 18: Code Should be Obvious

  • Obscure code requires a lot of time and energy to understand and raises the chances of bugs and misunderstandings. The long-term cost of maintenance outweighs any short-term benefits to the code author.
  • C# tuples and Java Pairs return multiple values from a method but increase obscurity, especially since their extractors are generic (e.g., getValuegetKey). It is better to introduce new strongly-typed types that eliminate all ambiguity.
  • I do not fully agree with the recommendation to always use the same types for allocation and declaration. While I agree with the obscurity; I think using interfaces helps with deeper modules; a good example is using IEnumerable in method signatures but casting it to a List in the method body. 

Chapter 19: Software Trends

  • The author posits that TDD focuses on getting features working (tactical) at the expense of finding the best design (strategic).
  • I like his perspective that getters/setters mostly add clutter.
  • Whenever a new software development paradigm pops up, challenge it; does it minimize complexity? Think about obscurity, complexity, cognitive overload, bloat, etc.

Chapter 20: Performance

  • This chapter read like a tack-on to meet book-length requirements. 
  • Examples of expensive calls:
    • A round trip network call within a data center can take 10 – 50μs (which is 10k x instruction times)
    • I/O to secondary storage (disks) can take 5 – 10ms (millions of instructions time); newer storage can be as fast as 1μs (which is about 2k instruction times).
  • Performance might not be essential for special cases, so ensure the critical path is clear and handle edge cases with simplicity.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.