Author: Dan North Publishing/Release Date: February 10, 2022 Watched/Read: February 21, 2022 Link: https://dannorth.net/2022/02/10/cupid-for-joyful-coding/

“Habitability is the characteristic of source code that enables [people] to understand its construction and intentions and to change it comfortably and confidently.

“Habitability makes a place liveable, like home.”

Habitability and Piecemeal Growth1, Patterns of Software pp. 7-16, Richard P. Gabriel

Composable

Small surface area

Code with a narrow, opinionated API has less for you to learn, less to go wrong, and less chance of conflict or inconsistency with other code you are using.

He goes on to say:

This has a diminishing return; if your APIs are too narrow, you find yourself using groups of them together, and knowing “the right combination” for common use cases becomes tacit knowledge that can be a barrier to entry.

There is a strong connection here between this idea of a small surface area, and the idea of deep modules from A Philosophy.

Unix philosophy

The Unix philosophy says to write [components] that work together well, described in the Composability property above, and that do one thing and do it well. For instance, the ls command lists details about files and directories, but it does not know anything about files or directories! There is a system command called stat that provides the information; ls is just a tool for presenting that information as text.

Single purpose vs. single responsibility

At first glance, this property sounds a lot like Single Responsibility Principle (SRP), and there can be some overlap depending on your interpretation of SRP. The difference is “Doing one thing well” is an outside-in perspective, considering something having a specific, well-defined and comprehensive purpose. SRP on the other hand, is an inside-out perspective, concerned with the organisation of code.

The SRP, in the words of Robert C. Martin, who coined the term, is that [code] “should have one, and only one, reason to change.” The example in the Wikipedia article is a module that produces a report, in which you should consider the content and format of the report as separate concerns which should live in separate classes, or even separate modules.

…in my experience, this creates artificial seams, and the most common case is where the content and format of the data change together; a new field, for instance, or a change to the source of some data that impacts both its content and the way you want to display it.

He mentions a second common scenario wherein SRP mandates UI components separate rendering and business logic. In practice this just means that a lot of changes affecting this UI component with require code changes across these multiple files each time. He notes

The greater risk is that this may be a premature optimisation preventing a more natural separation of concerns emerging as the codebase grows, and as components emerge that “do one thing well” and that are better suited to the domain model of the problem space.

Further

As any codebase grows, the time will come to separate it into sensible subcomponents, but the properties of Composability and Domain-based structure will be a better indicator of when and how to make these structural changes.

Predictable

Code should do what it looks like it does, consistently and reliably, with no unpleasant surprises. It should be not only possible but easy to confirm this. In this sense, predictability is a generalisation of testability.

Predictable code should behave as expected, and should be deterministic and observable.

Behaves as expected

The first of Kent Beck’s four rules of simple design is that the code “passes all the tests”. This should be true even when there are no tests! The intended behaviour of predictable code should be obvious from its structure and naming. If there are no automated tests to exercise this, it should be easy to write some.

He goes on to suggest that some people think of test-driven development as a religion rather than as a tool. He shares an anecdote wherein he once worked on a complex trading system with ~7% test coverage. He points out that much of the code had no tests at all, but there were parts of the system identified as particularly complex or high risk that had much more expansive and sophisticated test coverage.

Deterministic

Software should do the same thing every time. Even code designed to be non-deterministic—say a random number generator or a dynamic calculation—will have operational or functional bounds that you can define. You should be able to predict memory, network, storage, or processing boundaries, time boundaries, and expectations on other dependencies.

Determinism is a broad topic. For the purposes of predictability, deterministic code should be robust, reliable, and resilient.

  • Robustness is the breadth or completeness of situations that we cover. Limitations and edge cases should be obvious.
  • Reliability is acting as expected in situations that we cover. We should get the same results every time.
  • Resilience is how well we handle situations that we do not cover; unexpected perturbations in inputs or operating environment.

Observable

Code should be observable in the control theory sense: we can infer its internal state from its outputs. This is only possible when we design it in. As soon as several components are interacting, especially asynchronously, there will be emergent behaviour and non-linear consequences.

He provides a four-stage model for gauging observability, with bonus stages:

  1. Instrumentation is your software saying what it is doing.

  2. Telemetry is making that information available, whether by pull—something asking—or push—sending messages; “measurement at a distance”.

  3. Monitoring is receiving instrumentation and making it visible.

  4. Alerting is reacting to the monitored data, or patterns in the data.

    Bonus:

  5. Predicting is using this data to anticipate events before they happen.

  6. Adapting is changing the system dynamically, either to preempt or recover from a predicted perturbation.

This is what I’ve been harping on about at D for ages: the standard approach is to deploy into production and hope for the best. There isn’t a precedent for having visibility of the health of a deployable once its in the wild. Thus, discovering issues is often done by the customers and the response is very knee-jerk — often resulting in a rollback.

Idiomatic

Code should conform to some idiomatic form. Some languages have strong opinions about how code should look, and sometimes even what code to write. Other languages are less opinionated, making it necessary for the developers themselves to choose a style and stick with it. Go and Python are two examples of opinionated languages, that have an idiomatic style of writing already codified by the community.

Python programmers use the term “pythonic” to describe idiomatic code. There is a wonderful Easter egg that appears if you import this from the Python REPL, or run python -m this from a shell. It prints a list of programming aphorisms called “The Zen of Python”, which includes this line, capturing the spirit of idiomatic code: “There should be one—and preferably only one—obvious way to do it.”

The Go language ships with a code formatter called gofmt which makes all source code look the same. This eliminates at a stroke any disagreements about indentation, braces placement, or other syntactic quirks. It means that any code examples you see in the library docs or tutorials look consistent. They even have a document called Effective Go that showcases idiomatic Go, beyond the language definition.

Then there are completely un-opinionated languages like C# and JavaScript. Deliberately multi-paradigm, these languages provide multiple ways to achieve the same goal, and none is more correct than another.

For something as simple as processing a sequence of values, most of these languages let you:

  • use an iterator
  • use an indexed for-loop
  • use a conditional while-loop
  • use a function pipeline with a collector (“map-reduce”)
  • write a tail-recursive function

What this means is that for any reasonably sized codebase, you will probably find examples of most of these approaches. All this variety adds cognitive load when reading the code, impacting your ability to concentrate on the actual problem you set out to solve.

Code idioms occur at all levels of granularity: naming functions, types, parameters, modules; layout of code; structure of modules; choice of tools; choice of dependencies; how you manage dependencies; and so on.

Local idioms

If a language has no community agreement around idiomatic style, it is up to you and your team to decide what “good” looks like. Automated enforcement of these patterns as much as possible is ideal. Make use of shared formatting rules in IDEs and linting tools.

Architecture Decision Records or ADRs, are a great way to document your choices about style and idioms. These are no less “significant technical decisions” than any other architectural discussion.

Domain-based

Software is written to fulfill a need. The need may be very specific, e.g. Accounting software: I want to be able to meet my current tax obligations.

Or the need may be very generic, e.g. an Operating System, a piece of software build to facilitate a wide range of more specific interactions.

Domain-based language

Programming languages themselves are a form of software, and are full of domain-specific language relating to computer science, things like hash maps, linked lists, database connections, and so on. They will have basic types comprising of integer, boolean, and character values. You could declare someone’s name as string[50] which may indeed be how it is stored, however, defining a Name type will be much more intention-revealing. Doing so also allows for name-related operations, properties and constraints. Many subtle bugs have arisen in banking software due to money being represented as floating point values; experienced financial software programmers will define a Money type with a Currency and an Amount, which itself is a compound type.

A marker of success with domain-driven code is that a casual observer cannot tell whether people are discussing the code or the domain. Indeed, well written domain-driven code makes it possible for non-programmers to be able to read and understand the code itself.

Domain-based structure

Using domain-based language is important, but the way in which code is organised can be just as important. Programming frameworks often offer a “skeleton project” with a suggested layout and stubbed files to help you get started. This structure, usually will not match the problem you are solving.

Consider the Model-View-Controller (MVC) pattern for example. Most implementations of this pattern prescribe a structure something like the following:

src
├── controllers
│   └── customer
├── models
│   └── customer
└── views
    └── customer

We can see we have one domain concept, that is scattered across multiple top level directories. It’s easy to imagine any non-trivial change requiring work in all three directories and possibly even more. Single Responsibility Principle (SRP) says that view code should be separate from controller code, and MVC implementations interpret this as having them in completely separate places. This disjointedness contributes to higher cognitive load, and creates unnecessary friction when making changes.

That’s not to say that we don’t need those distinctions in our artefacts, we will still need models and views and controllers. But we can group them in a more domain-based way, by keeping related domain concepts together:

src
├── customer
│   └── customer-controller
│   └── customer-model
│   └── customer-view
├── product
│   └── product-controller
│   └── product-model
│   └── product-view

Domain-based boundaries

When we structure our code in this way, module boundaries become domain boundaries, and deployment becomes straightforward. Everything we need to deploy a component as a single artefact is co-located. This structure lends itself to a monolith-based deployment, or microservices, or anywhere in between.