Tuesday, January 11, 2022

A Philosophy of Software Design

Notes for the book A Philosophy of Software Design by John Ousterhout (see also his home page.

Introduction

  • The author starts with an interesting thought: Writing computer software is one of the purest activities in the history of the human race.
  • The greatest limitation in writing software is our ability to understand the systems we are creating
  • Two general approaches to fighting complexity
    • Eliminate complexity by making code more simpler and more obvious
    • Encapsulate it
  • Incremental development
    • Software design is never done.
    • You should always be on the lookout for opportunities to improve the design of the system
  • Red flags - signs that a piece of code is probably more complicated than it needs to be.
    • One of best ways to improve your design skills: To Recognize red flags

What is complexity?

  • Book's definition

    Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

  • Crude mathematical definition of overall complexity of a system (C) with parts. (Though this kind of excludes interaction between parts - that will be addressed later)

  • Complexity is more apparent to readers than to writers.

  • Symptoms of complexity - Three general symptoms

    • Change amplification
    • Cognitive load
    • Unknown unknowns
  • A very important design goal for a system is to be obvious.

  • Two main causes of complexity

    • Dependencies - In this book: When a given piece of code cannot be understood and modified in isolation
    • Obscurity - When important information is not obvious

Tactical vs strategic programming

  • Tactical: Main focus to get something working
  • Strategic: Working code isn't enough but you need to produce good design
  • Recommendation to continually spend 10-20% of total development time on investment
  • Research data on this would be interesting
  • "Tactical tornado" - Quick progress leaving mess behind
  • A related quote from Uncle Bob: The only way to go fast is to go well

Deep vs shallow modules

  • For this book: A module is any unit of code that has an interface and an implementation. (class, method/function, ...)
  • Viewing each module in two parts: an interface and an implementation
  • Best modules are those whose interfaces are much simpler than their implementations
  • Interface has two parts of information: Formal and informal
  • An abstraction is a simplified view of an entity, which omits unimportant details
  • Abstraction can go wrong in two ways
    • An abstraction can include details that are not really important
    • An abstraction can omit details that really are important
  • Deep vs shallow modules
    • Deep modules: Powerful functionality (lots of functionality) with simple interfaces
    • Shallow modules: Ones whose interface is relatively complex compared to the functionality it provides (Red flag ๐Ÿšฉ)
  • Classitis: Syndrome stemming from mistaken view that "classes are good, so more classes are better"
    • Java Streams as an example
  • Interfaces should make the common case as simple as possible

Information hiding

  • Each module should encapsulate a few pieces of knowledge.
  • Information hiding reduces complexity in two ways
    • Simplifies the interface to a module
    • Makes it easier to evolve the system.
  • When designing a new module, you should think carefully what information can be hidden in that module.
  • The opposite of information hiding is information leakage.
    • One of the most important red flags in SW design (Red flag ๐Ÿšฉ)
    • Causes dependencies
    • Common cause: temporal decomposition
  • Note: Information hiding can often be improved by making a class slightly larger
  • Overexposure (Red flag ๐Ÿšฉ) - If API for the common case requires users to learn about rarely-used cases

General-Purpose Modules are Deeper (generality vs specialization)

  • Design decision: Whether to implement a new class/module in a general-purpose or special-purpose fashion.
    • In general, the author has found that specialization leads to complexity
    • Sweet spot: Implement new modules in somewhat general-purpose fashion: Functionality should reflect your current needs but interface should not.
  • Questions to ask yourself (when designing an interface)
    • 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 specialization upwards (or downwards)
    • General-purpose API, specific use of API
    • OTOH downwards: Device drivers - Very specific to devices but APIs are generic
  • Eliminate special cases in code
  • Summa summarum: Unnecessary specialization is a significant contributor to software complexity.
    • Whether in form of special-purpose classes/methods or special cases in code

Layers & abstractions

  • In a well-designed system, each layer provides a different abstraction from the layers above and below it.
  • (Red flag ๐Ÿšฉ) Pass-Through methods / adjacent layers with similar abstractions
  • Each new method should contribute significant functionality
  • Decorators - often pass-through methods, easy to overuse
  • Pass-through variable - another form of API duplication - a variable passed down through a long chain of methods

Pull Complexity Downwards

  • It is more important for a module to have a simple interface than a simple implementation (deep modules)
  • E.g. configuration parameters might be an easy excuse to avoid dealing with important issues - though also needed in many cases
  • When developing a module, look for opportunities to take a little bit of extra suffering upon yourself to reduce the load of your users.

Better Together Or Better Apart

  • Given two pieces of functionality, should they be implemented together or separate?
  • The decision should reduce the complexity of the system as a whole and improve its modularity.
  • Some guidelines
    • Bring together if information is shared
    • Bring together if it will simplify the interface
    • Bring together to eliminate duplication
      • (Red flag ๐Ÿšฉ) Repetition
    • Separate general-purpose and special-purpose code
      • (Red flag ๐Ÿšฉ) Mixing special/general-purpose code
  • (Red flag ๐Ÿšฉ) Repetition
  • Method length - Author's opinion: Length by itself is rarely a good reason to split up a method
    • Additional interfaces -> additional complexity
    • When designing methods, most important goal should be to provide clean abstractions
    • Each method should do one thing and do it completely.
    • (Red flag ๐Ÿšฉ) Conjoined methods
  • A different opinion: Uncle Bob's Clean Code

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.

Define Errors Out Of Existence

  • Summary: Reduce the number of places where exceptions must be handled
  • Exception here - any uncommon condition that alters the normal flow of control in a program
  • Two typical approaches to deal with exceptions
    • Move forward and complete the work despite the exception
    • Abort the operation in progress and report the exception upwards
  • Exceptions make interfaces more complex - Classes with lots of exceptions are shallower than classes with fewer exceptions
  • Ways to reduce the number of places to handle exceptions
    • Define errors out of existence
    • Mask exceptions
    • Exception aggregation (both this and masking position exception handler where it can catch the most exceptions)
    • Just crash?

Design it Twice

  • When designing a piece of software, design it twice - consider multiple options.
  • If you're accustomed to solving problems with the first quick idea, it doesn't typically work with harder problems
  • Avoid the fallacy of "smart people get it right the first time"

Why Write Comments? The Four Excuses

  • The process of writing comments, if done correctly, will actually improve a system's design
  • Four excuses
    • Good code is self-documenting
      • If users must read the code of a method in order to use it, then there is no abstraction
    • I don't have time to write comments
      • Vs. long-term investment mindset
    • Comments get out of date and become misleading
      • Organizing the documentation to be as easy as possible to keep it up-to-date
      • Code reviews
    • All the comments I have seen are worthless
      • See the next sections
  • Benefits of well-written documents - Overall idea - Capture information that was in the mind of the designer but couldn't be represented in the code
  • A different opinion from Uncle Bob: ... comments are, at best, a necessary evil...

Comments should describe things that aren't obvious from the code

  • Comment categories
    • Interface
      • For users of the interface
      • Note: Separate interface/implementation comments
      • (Red flag ๐Ÿšฉ) Implementation documentation contaminates interface
    • Implementation comment
      • Main goal: Help readers understand what the code is doing (not how it does it)
      • Also why
    • Data structure comment
    • Cross-module comment - Describing dependencies
  • Pick conventions
  • Don't repeat the code
    • (Red flag ๐Ÿšฉ) Comments repeating the code
  • Lower-level comments add precision
  • Higher-level comments enhance intuition
  • Document cross-module design decisions - "Design notes" documentation (that can be referenced from comments in code)

Choosing names

  • Bad names create bugs
  • Names are a form of abstractions
  • Names should be precise
    • (Red flag ๐Ÿšฉ) Vague names
    • (Red flag ๐Ÿšฉ) Hard to pick name - a hint that the underlying thing may not have a clean design
  • Use names consistently
  • Avoid extra words
  • A different opinion from the Go style guide on variables: "Keep them short; long names obscure what the code does."

Write The Comments First

  • Write the comments first
  • Comments as a design tool, "Comment-driven design"
  • (Red flag ๐Ÿšฉ) Hard to describe - a hint that the underlying thing might be a problem with the design of the thing you're describing

Modifying Existing Code

  • Improving & cleaning as changing code: Ideally, when you have finished with a change, the system should have the structure it would have if you had designed it from the start with that change in mind.

Consistency

  • Consistency applies at many levels is a system, e.g.
    • Names
    • Coding style
    • Interfaces
    • Design patterns
    • Invariants
  • How to ensure consistency?
    • Document
    • Enforce
    • "When in Rome, do as the Romans do"
    • Don't change existing conventions - Even if the new idea would be better, value of consistency over inconsistency is almost always greater than value of one approach over another

Code should be obvious

  • Code being obvious: One can read the code quickly, without much thought, and their first guesses about the behaviour or meaning of the code will be correct.
  • "Obvious" is in the mind of the reader - It's easier to notice that someone else's code is non-obvious than to see problems with your own code.
  • (Red flag ๐Ÿšฉ) Non-obvious code
  • Software should be designed for ease of reading, not ease of writing.
  • To make code obvious, ensure that the reader always has the information they need to understand it.
  • Inheritance
    • Should be used with caution
    • Consider composition over inheritance
  • Agile development
    • Risk of focusing to features, not abstractions
    • Risk of encouraging developers to put off design decisions in order to produce working software ASAP
    • The increment of development should be abstractions, not features
  • TDD
    • The writer states that TDD would focus on getting features working instead of finding the best design
    • The writer kind of ignores "refactor" step typically stated important with TDD
  • Getters and setters
    • Although it may make sense to use getters/setters if you must expose instance variables, it's better not to expose instance variables in the first place.

Designing for Performance

  • Measure before (and after) modifying
  • Design around the critical path

Decide What Matters

  • Separate what matters from what doesn't
  • Structure software systems around the things that matter.