TechWorkRamblings

by Mike Kalvas

202602021747 A Philosophy of Software Design

#source #wip #new

Preface

Writing software is the practice of problem decomposition. This book is about strategies to achieve that goal while reducing complexity.1 If a design pattern or strategy doesn't actually reduce complexity, simply don't use it, but a great deal of thought and experience has gone into this book so think carefully before passing judgement.

Chapter 1: Introduction (It's All About Complexity)

  • Simpler designs allow us to build larger and more powerful systems before complexity becomes overwhelming. (pp. 1)
  • The first approach to fighting complexity is to eliminate it by making code simpler and more obvious. (pp. 2)
  • The second approach to fighting complexity is to encapsulate it so programmers can work on the system without being exposed to all its complexity at once. (pp. 2)
  • Software developers should always be thinking about design and reducing complexity is the most important aspect of design; therefore, software engineers should always be thinking about complexity. (pp. 3)

Chapter 2: The Nature of Complexity

  • The ability to recognize complexity is a crucial design skill. (pp. 5)
  • It's easier to be able to tell if a design is simple than it is to create a simple design. (pp. 5)
  • Definition of 202602022023 Complexity
  • Isolating complexity in a place where it will never be seen is almost as good as eliminating it entirely. (pp. 6)
  • Complexity is more apparent to readers than to writers. (pp. 6)
  • Complexity manifests itself in three ways, each of which make it harder to carry out development tasks (pp. 7)
    • Change Amplification — simple changes require making changes in many places. Change amplification can be annoying but if it's clear what code needs to be changed, the change will be successful and bug-free. (pp. 7, 9)
    • Cognitive Load — a developer needs to know a great deal about the system as a whole to complete a simple task. Sometimes an approach that requires more lines of code is actually simpler because it reduces cognitive load. High cognitive load will increase the time and cost of a change, but if the information required is clear, it's a matter of spending that time doing it in order to make a successful, bug-free change. (pp. 7, 9)
    • Unknown unknowns — it is not obvious what code needs to be modified or even what a developer must know in order to complete the task successfully. Of the three manifestations of complexity, unknown unknowns are the worst. There is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won't find out about it until bugs appear after you make the change. There is no solution to this. Reading every line of code in the system is as good as we can do, and even then external factors can also manifest here that we have no ability to foresee. (pp. 8, 9)
  • One of the most important goals of the design of a system is to be obvious.
  • Complexity is caused by two things: dependencies and obscurity (pp. 9)
    • A dependency exists when it's not possible to understand and modify a given piece of code in isolation. Dependencies lead to change amplification and high cognitive load. (pp. 9, 11)
    • Obscurity is when important information is not obvious. Obscurity creates unknown unknowns (pp. 10, 11)
    • These two are often combined when dependencies are non-obvious
  • If a system has a clean and obvious design, it needs less documentation
  • Complexity is incremental and accumulates in small pieces everywhere. The only viable strategy for combating it is a zero-tolerance policy. (pp. 11)

Chapter 3: Working Code Isn't Enough (Strategic vs. Tactical Programming)

  • A strategic approach to programming produces better designs and is actually cheaper and faster than the tactical approach in the long run. (pp. 13)
  • The tactical approach to programming focuses on getting something working. It is short term and leads to one small compromise after another, focusing on speed and ending up in a catastrophically complicated system. There is an organizational failure mode when the fastest and messiest developers are valued over engineers who take their time to do things correctly for the long run. (pp. 13–14)
  • The strategic approach focuses on the long term health and design of the system. It knows that working code is not enough favoring producing great designs as an investment (202304112152 Intellectual investment is like compound interest, 202109251140 Play long term-games). (pp. 14–15)
  • Spend 10-20% of your time investing in good design. It will pay for itself in the long run when your development is more than 10-20% faster. Conversely if you don’t invest you’ll pay for it double by being at least 10-20% slower. (pp. 16)

Chapter 4: Modules Should Be Deep

  • In an ideal world, there would be no dependencies between modules at all, but this isn’t possible. Therefore we should strive to minimize dependencies between modules as much as possible. (pp. 19)
  • If a module’s interface is much simpler than its implementation, there will be a great deal that can change without affecting modules that depend on it. (pp. 19)
  • Interfaces can be explicit or implicit. Making interfaces clear and explicit (related to 202506201106 Make invalid states unrepresentable) reduces “unknown unknowns.” (pp. 21)
  • 202602211049 Abstractions
  • The benefit of using a module is the functionality it provides. The cost is its interface and the introduction of a dependency. Therefore, modules should provide deep functionality via a small interface. (pp. 23)
  • A shallow module is one whose interface is complicated relative to the functionality it provides. Small modules tend to be shallow. Shallow modules do not help in the fight against complexity. (pp. 25)
  • Interfaces should be designed to make the common case as simple as possible. (pp. 27) (Do I fully agree with this in all cases?)

Chapter 5: Information Hiding (and Leakage)

  • Information hiding is the technique of encapsulating knowledge or design decisions, embedding them into the implementation but not exposing them in the interface. These are usually “ implementation details” or assumptions about the problem space. Information hiding simplifies the interface of a module and makes it easier to evolve the system. (pp. 29–30)
  • Information leakage is when information is exposed outside of the module resulting in the design being depended on in multiple modules. Put another way, leakage occurs when the same information is used by multiple modules. By definition, the interface of a module is made of leaked information. Leakage can be implicit too, if for instance functions rely on a specific file format implicitly. (pp. 30–31)
  • Temporal decomposition is a failure mode where design is based on the order (time) that tasks are performed. This is a common mistake.
  • When designing modules, focus on the knowledge needed to perform each task, not the order in which tasks occur. (pp. 32)
  • Sensible defaults are a example of good partial information hiding. "The best features are the ones you get without even knowing they exist." It should be possible to make the common case easy while exposing escape hatches for the more advanced cases which people would need to understand anyway to opt into. (pp. 36)

Chapter 6: General-Purpose Modules are Deeper

  • Modules should be "somewhat general-purpose", defined as specific to the current problem in their implementation but general enough to support multiple uses. (pp. 39)
  • The right amount of generality for a module increases its usefulness. If we make the module general enough, it can be used for a variety of use cases, but if we make it too general it becomes hard to use for any use cases. (pp. 43)
  • Ask yourself: (pp. 44)
    • What is the simplest interface that will cover all my current needs?
    • In how many situations will this method be used?
    • Is this API easy to use for my current needs?
  • Push specializations up and down (onion). (pp. 45)
  • Eliminate special cases in code. (pp. 48)

Chapter 7: Different Layer, Different Abstraction

  • Adjacent layers of a program having the same level of abstraction is a red flag. (pp. 51)
  • Pass-through methods typically indicate that there is not a clean division of responsibility. (pp. 52)
  • Use wrappers and decorators sparingly; they are shallow classes that don’t add functionality but do increase complexity. A good example of a wrapper is for translating your internal interface to an external one you don’t control. (pp. 55–56)
  • Try to eliminate pass through variables but this can be challenging. Contexts might help but another solution in my opinion is to simply refrain from making code too deep. (pp. 57–59)

  1. Ousterhout, J. K. (2021). A philosophy of software design (Second edition). Yaknyam Press.