Skip to content

Instantly share code, notes, and snippets.

@Frikki
Created April 7, 2017 11:45
Show Gist options
  • Save Frikki/3abc8255acb19fb9a693c14cfa824d2d to your computer and use it in GitHub Desktop.
Save Frikki/3abc8255acb19fb9a693c14cfa824d2d to your computer and use it in GitHub Desktop.
Fit Software in Your Head

Fit Software in Your Head

This article is a write-up on Dan North’s talk Software That Fits In Your Head at GOTO Amsterdam 2016.

Abstract

Software gets complicated fast. Most of good architecture and design practise is about trying to slow the rate at which software gets complicated. You can’t stop it, it’s a form of entropy. You can only slow it down and do your level best to stay on top

The Goal of Software Development

The goal of software development is minimising lead time to business impact. It turns out that minimising lead time to business impact isn’t that hard. What is hard is to sustainably minimise lead time to business impact, which is the goal of what we call software development.

The Goal Is Not to Produce Software

We call ourselves software engineers. That’s like a plumber calling himself a pipe engineer. The purpose of plumbing isn’t pipes. The purpose of plumbing is heating and water. The purpose of what we do is information. We used to call it information technology, i.e., applying technology to information, but that wasn’t cool enough, so now we are software engineers, and some of us are software craftsmen, which is kind of cute. However, software is not the point – business impact is the point. We don’t want to make more software, we want to make business impact. We want to make business impact quickly. Quickly suggests less software.

Productive ≠ Effective

Productive doesn’t equal effective, which is a shame, because for decades we’ve been talking about software productivity metrics, but it’s the wrong thing. The problem is this:

Code Is Not an Asset

We think of code as an asset. We use languages like legacy code or legacy system. Legacy is a thing of value you leave to the next generation. So we think of software as valuable. We talk about technical debt because good software is an asset and bad software is debt. The uncomfortable truth is that code isn’t an asset – code is the cost. Code is the cost of getting the business impact, and business impact is the thing we care about.

Code Has Costs in Many Different Ways

  • Writing code costs. The cost of having human beings.
  • Waiting for code costs, which is the cost of delay.
  • Changing code costs. Maintaining code costs.
  • Understanding code costs, which is the single biggest cost within changing code.

Understanding Code

There are three kinds of code. There is code I know, because I wrote it. I’m pretty comfortable about that. I wrote it recently enough that it’s still there.

Then there is code that’s been around and is stable, i.e., code everyone knows. It’s well-documented and all that stuff. These two we are happy working with.

Then there’s this awkward middle-gap code no one knows. It’s been around long enough that it got old, but no one noticed it getting old. We didn’t do anything deliberately about it getting older, it just got older. If you look at a histogram of that code, you tend to find that a huge chunk of code fits in that middle gap.

Code Should Be Stabilised or Killed Off

And it should be stabilised or killed off fast. If we can get into the habit of stabilising or killing off code fast, we end up with only code that we wrote recently or code that is really stable and well-known. It sounds really obvious, but it is really hard to do deliberately, because of entropy, i.e., the disappointing default state is that stuff just gets older.

Two complementary patterns of software

  • short software half-life, and
  • fits in my head

can help reasoning about stabilising or killing off code.

Short Software Half-Life

Half-life of software is a mental model. Half-life in physics is essentially if you have a million atoms of a radioactive isotope, depending of what type of element it is, you can know exactly how long it will take for half of those atoms to decay to a more stable state. What you don’t know is which atoms. If you have a single atom, then, after that half-life, it’s either decayed or hasn’t decayed, and in fact, it is in this weird state where it’s both of these things until you look at it, which drives Schrödinger’s cat mad.

Think of the code that you are currently working on, and think how long you need to go away for, so that when you come back, half the code has changed. Half of the code has been changed beyond recognition, and half hasn’t. How long would you need to go away for?

If we think about half-life in software, it is typically years, maybe decades. We want code to change much faster – in a few weeks or less. To achieve that, we must fearlessly delete and rewrite code, because the premise is: I now know more and I can get from public static void main to something that I want faster than I can understand, manipulate or change this code to make it do what I want. And the next time I write it, it won’t have any of these speculative things that I put in and didn’t end up using.

Enabling Design Considerations

We can’t just take a huge monolithic and tightly coupled code and confidently start to delete half of it. To do that, we need to:

  • write discrete components,
  • define a component boundary,
  • define a component purpose, and
  • define its responsibility.

Write Discrete Components

We need to build small, separate pieces. When you have a discrete component, you can reason at a component scale.

Define Component Boundary

We need to define what happens at the edges. If you can think about what happens at the edges, you own the centre.

Define Component Purpose

What is the point of this software? Code self-documents what it does. Well-written, cleanly-written code tells you what it does. But it doesn’t tell you why it’s there – it doesn’t tell you its purpose. And it doesn’t tell you what isn’t there. It doesn’t tell you all of the design considerations – all of the other ways of doing it that were thought through and were discarded. It doesn’t show the iterations it took to get there. We can pour over version control if we want to, but when we look at code, the code is telling us what it is. It’s not telling us what it isn’t and it’s certainly not telling us why it is.

If we don’t have somewhere – vividly captured – the purpose of that component, we cannot reason about it. We can try to infer it by looking at the code, but what we derive may be wrong.

Define Component Responsibility

Responsibility is slightly different from purpose. Purpose is why it’s there – responsibility is what it owns and, going back to boundary, what it doesn’t own. If we are going to change it or replace it, we need to know what it is, why it’s there, and what it’s responsible for. Once we understand these things, we can start to reason about it.

Stewardship Considerations

There is also a life-cycle element for components in terms of: we dreamt them up, we decided we needed them, we started writing them, we introduced them. So now we need to think about how we will look after them. In terms of stewardship, we need to:

  • write component tests and documentation,
  • optimise for replaceability,
  • expect to invest in stabilising, and
  • build a stable team.

Component Tests and Documentation

When we do specification by example, programming by example, or TDD, those things aren’t tests. They are executable specification. We can use them to verify some of the behaviour of the software, but that is a side effect. We haven’t got tests for our code, yet. Don’t fool yourself.

Documentation should tell us why something is there, the decisions that were made. Michael Nygard has a blog post about architecture decision records (ADR) – read it! Essentially, it’s a really simple markdown that says: here’s the decision we made, here are the people who made it, here are the things we considered, and here are the choices we decided not to do, so here is what we ended up with, and for extra credit, here are some of the risks it introduces. And that stays in version control, so we can look at the decisions we made over time. This is different than a wiki, which holds current state.

Optimise for Replaceability

Typically, we should not introduce libraries that couple components together. If we do that, then bad things can happen. Yes, we reduce duplication, which is good, but instead we introduce coupling, which is not great. If we optimise for replaceability, we might make different decisions.

Invest in Stabilising

If a component is going to be long-lived and it’s going to be something that someone wants to come along and replace, then having documentation and tests around it, and bothering to invest some time in it become useful.

Build a Stable Team

If all our code is changing on a frequent basis, the information and knowledge about what that code does is in the team. Therefore we want to invest in building a stable team, which is different from building a static team.

If you want to go fast, travel alone. If you want to go far, travel together. If you want to go far quickly, pack light. ~ African proverb, modified by Dan North

Fits in My Head

This is based on James Lewis, who also coined the term micro-services. James complexity matrix is that he doesn’t like to look at code that is bigger than his head, which is about a screen of code. If you have to page up and down to look at something, it’s probably to complicated… be it a function or a method or whatever else it might be. It’s about whether you can reason about something. The obvious logic is that you can only reason about something that fits in your head. The converse of that is this: if you can’t fit something in your head, you can’t reason about it. A coping mechanism we apply is to divide and conquer.

With big, ugly software systems, rather than simplifying them, because that’s time and effort and hard, we come up with techniques for actively ignoring parts of it. TDD is a fantastic way for actively ignoring stuff that’s going on around you. Or to qualify that, mocking and stubbing is a fantastic way of actively ignoring things happening around you. Where you have tests with lots of stubs and mocks, what that should be telling you is that in order to reason about this piece, I have to ignore loads of other stuff, which suggests it’s coupled to loads of other stuff, which is bad. Mocks are basically painkillers. Mocks are drugs to ease the pain of the complexity of the system. But we should think about the complexity of the system. If removing the mocks inhibits you from testing the system, the system must be broken up so you can.

Multiple Dimensions

Fits in my head is multi-dimensional. So we want a technology that fits in our heads. Some languages fit better in our heads than others. For example, Clojure is homo-iconic vs Scala – the least opinionated language ever – that allows you to write code in any way you want. The less opinionated the language is, the more dialects you get from programmers. Thus, as a team, we need to be much more opinionated about how code is written, because we want a consistent code base.

Multiple Scales

Fits in my head happens at multiple scales. We want to look at any code, and that code should fit in our heads. If we have a consistent style of code, we can look at anyone’s code without having Jimmy’s style and Bob’s style of writing code, which will otherwise force us to switch to their modes. Interestingly, different styles produce different types of bugs. We want consistency across multiple scales. When we look at a class, it should look like we wrote it ourselves. When we pan back and look at a component, it should fit in our heads. When we look at a bunch of different components talking to each other, they talk in a consistent way, and so that should fit in our heads. We can then reason about a system. If it’s built consistently, then something we learn over there, we can apply over here, which reduces our cognitive load.

What Would James Do?

When we start getting slightly larger and start scaling, what happens is that there’s a bunch of different teams and all the agile stuff tells you that the team is the unit of delivery, teams are autonomous, and teams should make their own decisions. In systems theory terms, it is called local optimisation, and local optimisation is fatal to any kind of global delivery. Any kind of global optimisation will be sabotaged by local optimisation.

A good way to get consistency is to have James in all the meetings, except a) it sucks to be James, and b) James is now a massive bottleneck. Instead, we could all wear our badges: What would James do? and thereby creating some shared values and guidelines, so we all can make consistent decisions.

Contextual Consistency
  • agree on guiding principles,
  • agree on idioms,
  • difference is data, and
  • familiarity ≠ simplicity

Now we can build a replaceable component architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment