TLDR
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., getValue, getKey). 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.