Notes on “A Philosophy of Software Design”

What’s the most important general idea in software?

  • Knuth: layers of abstraction
  • Ousterhaut: problem decomposition

Even though it’s the most important idea, there is no course where problem decomposition is the central idea of the course.

Simple rule: Abstractions should be deep

The width of an abstraction is how big the interface is. The size of an interface is how much the user needs to know in order to use it. This includes public functions, side effects, dependencies, and other context like tribal knowledge.

Width should be as small as possible and the depth should be as large as possible.

Depth is the functionality of the class – the value it adds, the complexity that it hides behind it’s interface.

A shallow class has a larger width than depth. It approaches the point where the thing has a higher cost to use than the value it provides.

A deep class has a larger depth than width. It has a clear ROI.

“Abstraction provides a simple way to think about something that’s quite complicated underneath.”

You can evaluate any layer of the system in terms of shallowness and depth – functions, classes, interfaces, libraries, frameworks, protocols, components, systems, products, businesses.

Does your function actually hide any complexity? Or does it expose all the complexity within it?

Does the user have to understand the implementation to use your software? Then it’s useless.

When “it takes more keystrokes to invoke this method than to write the body itself”, it’s shallow.

One exception I can imagine is renaming an expression so as to make it easier to understand what the body is doing. Figure out an expression once, name it, then reuse it freely without having to figure it out again.

Inner platform effect is a symptom of a shallow abstraction.

Applies to writing, documentation, product, and marketing as well

“This is one of the biggest mistakes that people make: too many, too small, too shallow classes”

“Class-itis”: someone heard that classes are good, so they mistook it to mean that more classes are better.

My take: many small classes increase the surface area of the interface, increasing complexity

Classitis is rampant in the Java world

“In managing complexity, the common case matters a lot. We want to make the common case really simple”

Deepest, most beautiful abstraction is the Unix file I/O abstraction

Five deep methods: open, close, read, write, lseek

Good design seems obvious in hindsight. But it’s not as easy as it looks. Bad design is everywhere.

Before Unix, you had two different kernel calls for random vs sequential access. The Unix design was not obvious then.

My take – product examples of deep abstractions:

  1. Google Search (the best imo)
  2. Apple
  3. Netflix
  4. Uber (another one of the best)
  5. Ikea

Product examples of shallow abstractions:

  1. Windows
  2. Jira
  3. Facebook

“Define errors out of existence”

Common approach is to catch and throw as many errors as possible. Better approach is to define semantics such that errors are impossible. You learned this a lot at Kifi.

My take: throwing exceptions increases the width of your abstraction. Especially when they’re undeclared exceptions. These have one unit of width because the abstraction throws the error, another because the user needs to know that the abstraction throws the error on their own.

Idempotency is a great way to eliminate errors: the abstraction enforces a statement to be true. Doesn’t matter if it was already true or not. Context independence means a smaller abstraction.

File deletion outside of Unix (Windows) want possible if the file was open. Unix simplified this by allowing you to delete the file, and it would still be usable just wherever it was open. After it was closed everywhere it would actually be deleted.

Substring in Java is fragile: returns errors if indices are out of bounds. Fixed by returning the overlap between the indices – out of bounds is equivalent to the edge of the string.

“We’re going to try to keep people from making mistakes.” This philosophy is very difficult because there are many more ways to make mistakes than ways to do it right. So your interface expands to cover a breadth of use cases that don’t add value. Rather, make it really really easy to do the right thing and to run the common case.

When is it a good idea to throw exceptions? If you can’t carry out the contract with the user.

What matters and what doesn’t matter? Try to make what matters as little as possible, but no smaller than that.

Exceptions vs error values? Exceptions are most useful when you throw them the farthest. Each layer that an exception is thrown past is a layer of abstraction made simpler by the exception throwing policy. If you’re catching exceptions in methods you call, there’s not much value over a return value. My take: actually at that point it’s counterproductive, because the compiler won’t force the caller to handle the error.

Mindset is critical to good software design. Working code is not enough.

Tactical vs strategic programming. Tactical is short-sighted. Strategic is far-looking.

Mistakes in design add up until the weight of their complexity is too large to clean up.

“Tactical tornado” is an engineer who is very productive at shipping shoddy code that leaves a trail of complexity in their wake

Working code is not enough – you must minimize accidental complexity.

Tactical approach is concave, strategic approach is convex

His hypothesis is that the inflection point is after you forget why you wrote the code – then the accidental complexity is magnified.

Most startups and rushed projects are tactical. Facebook is tactical – Google is strategic.

You can be successful with crappy code. You can also succeed with great code.

Culture of quality attracts the best people. The best people produce the best results. So sacrificing on quality for speed may be first-order positive, but has negative second-order effects that will freeze long-term speed. Complexity outweighs the speed benefits and good people leave.

When writing new code, start with careful design and good documentation

When changing code, find something to improve. Make the abstraction the way it should have been from the start.

Small steps, not heroics. No rewrites, but careful and thoughtful pruning.

Making abstractions just slightly more general purpose can make them deeper and more valuable.

My take: I’ve seen general-purpose simplifications a few times. I also often see general-purpose complications. It’s an artistic move. Seems like it’s useful whenever making the abstraction general purpose separates the context from the function. Sometimes the context can muck up the function. So a general-purpose function plus a context-specific invocation is simpler than both combined in one.

Philosophy on hiring: hire for slope, not y intercept. Hire based on how someone is growing vs where they are today. Why?

Someone who can grow can add way more value than someone who is stagnant that has done the job before. Jobs change, requirements change, great performers adapt and poor performers don’t.

My take: how to measure slope?

  • What is the most recent new thing this person has done?
  • What is the delta between the job requirements and where the person is? Will the job require them to grow and do you estimate that they could grow into it? The larger the delta, the more value a “yes” answer matters as to whether they will add value to the team.

Conversely, as an individual, choose projects that you can grow into as much as possible. Failure to grow into the role means loss of value, but can still be hugely beneficial depending on the costs.

Same applies to many contexts. Set the most ambitious goals that a team can grow into. Create products / education programs / technologies with the steepest learning curve that users can grow into. Write / create content with the steepest engagement curve that the audience can grow into.

“My best hires wer the ones where I really enjoyed our conversation during the interview” I have the same feeling about teams I’ve joined