Post

Your Code Should Tell a Story

Your Code Should Tell a Story

A humorous kitten sitting on a laptop, trying to understand code.

Cognitive Load

When it comes to any code, Cognitive Load is the mental effort required for a person reading the code to understand the semantics of the program and your main intent. When reviewing someone else’s code I enter a very deep state of focus which helps me visualise the various states and behaviours represented within the code I’m reading. Sometimes, it’s easy to see how each line of code changes the system’s state. However, most of the time, understanding even a small section of a system requires that I keep so much information, constraints and caveats in my head that it becomes genuinely tiring. This exhaustion is what we mean by ‘cognitive load’.

A fairly well-known aspect of software development is that:

Code is harder to read than it is to write

And this is often true even for your own code. Crucially though, code is read by a human person far many more times than it is written or modified. It is for this reason that the most important part of a software developer’s job is to write code that will be easy to read by someone else. That code must impose the absolute minimum amount of cognitive load on its readers.

Developers are Story-tellers

There are many ways to ensure that code does not require unreasonable amounts of cognitive load. The ‘clean code’ movement, after all, is all about achieving that goal. But, I don’t think it’s healthy to reduce all readability problems to mere patterns and recipes to be followed. It’s good to learn design patterns and keep them in our toolbox, ready to be wielded when the situation calls for it. However, it’s essential that developers are able to think creatively and outside the box because every situation requires its own solution.

The best way I can describe the process of writing understandable code is by comparing it to writing a story. A code/story if you will. Just like in books, the code/story should take the reader on a cognitive journey to understand the author’s original intent and vision. Unlike a standard novel however, anyone writing code/stories has a very powerful tool at their disposable. In code/stories, the genre is more akin to a choose-your-own-adventure book than anything else. The reader has the option, at any time, to delve into a lower level of abstraction where the details get more intricate and intense. They also have the option to return to a higher level of abstraction or move on to the “next chapter,” so to speak, at their discretion.

The Tools of Story-telling

All programming languages give software developers a pangaea of tools that they can use to control exactly what code/story is told and to limit a reader’s cognitive load. For example, in Java, we have the ability to define packages (and more recently, even higher-level modules), and within those packages we are able to define classes and interfaces. Accessibility qualifiers assigned to the various members of those classes and interfaces can be used to “hide” and limit information from the various other components of the code.

Like every other Java programmer, I was told that every class should appear in its own file and that the layer that class lives in should be represented by the package it lives in… that was “good practice.” More recently, however, I’ve started realising that this organisation structure does not make for a good code/story. Let’s imagine we are writing a bookkeeping system, made up of various financial accounts: almost all Java developers would probably use an organisational structure similar to the following:

  • Package controller
    • Class AccountController
    • Class TransactionController
  • Package entity
    • Class Account
    • Class Transaction
  • Package service
    • Class AccountService
    • Class TransactionService

This is all very standard and widespread through the industry. But does it tell a good code/story? Have you ever read a novel that is organised into sections called “Characters”, “Locations” and “Happenings”? Not really. I think this enterprise-like way of organising items requires an extreme amount of cognitive load:

  • If I want to know what domain objects are handled by this system, should I look in the controller, entity or service package?
  • Since Transactions are visible at the top-level, can we create Transactions without having an Account?
  • If I want to create a ‘Deposit’, do I have to go through the Account or the Transaction objects?
  • When changing something in the Account entity, do I also need to remember to check all the other packages for breakages?

Let the Story Flow

If I had full control about what the file structure looks like, I would go with:

  • Package account
    • Package transaction
      • Class Entity (or Transaction)
      • Class Service (or TransactionService)
    • Class Entity (or Account)
    • Class Service (or AccountService)
    • Class Controller (or AccountController)
      • Inner Class Response (or AccountResponse)
      • Inner Class CreateRequest (or CreateAccountRequest)

Using this structure, the code already feels more like a story; it has a defined start and a defined end. If we are within the account scope and package, the reader has the option to descend to a lower level of abstraction and read more about transaction. But, if they wish to move on to a different domain object (let’s say, they want to read about “exchange rates”), they can forget about all the items related to accounts (and thereby, transactions) freeing up their headspace for that code/story instead. As you can see, by using this organisational structure, we control exactly the cognitive load that the reader experiences across their journey.

Notice that the proposed structure above allows us to shorten the name of the classes, without having to repeat prefixes. Modern IDEs will search within the fully qualified class name when you type something like account.Entity. Be mindful however, that in certain teams, changing the way you name components (in addition to how you organise them) may be too large of a shock. It’s better to introduce changes bit-by-bit and definitely always try to remain consistent at a team level (don’t change anything without your team’s buy-in).

Recently, I’ve also started using inner classes as an additional layer of organisation; this is something that I’ve picked up from Rust and its very flexible organisational constructs. In the example above, the Response and CreateRequest classes are data transfer objects (DTOs) that represent the structure of messages travelling across an API. They are only relevant if the reader is looking at the API (which is implemented inside the Controller class). These inner classes, I feel, are really useful to round out the code/story and finely control the reader’s cognitive load.

Coupling and Cohesion

After I started treating code like writing a story, I realised that the solution to the cognitive load problem I described at the start also tackles the problem of coupling and cohesion.

Coupling and cohesion are scientific terms that can be applied to code to get a real metric for the health of the code. Two different components are highly coupled together if making changes within one component requires making changes within the other. Good system design tries to reduce coupling between components to the bare minimum that is required. On the other hand, the cohesion of a component measures the indivisibility of that component. If all the constituent parts of a component actually depend on each other, and cannot be removed, then the component is said to be highly cohesive. Good system design tries to maximise cohesion.

In our previous accounting example, the organisational structure allows us to finely control the cohesion and coupling of the system. If we want to limit coupling between Transactions and any external components, then we can decree that packages cannot reach into the nested packages of other top-level packages. This automatically prohibits the use of the Transaction package outside of the Account package and we can enforce this in Java with some special build-time tools (like the architecture rules engine in SonarQube).

The cohesion of the system is also very tightly enforced. The Response and CreateRequest classes are not needed outside of the Controller. They don’t even make any sense if they happen to end up on their own as a top-level class. The inner class relationship allows us to treat the Controller and its data transfer objects as one single cohesive component.

Weave a Beautiful Story

With the advent of generative AI that can quickly generate code, the ability to tell a good story with code has become indispensible. When looking through AI-generated code, I find that it often misses that quality which would allow someone, in the future, to come in and make improvements or fix issues within the code. It just gets tiring trying to follow the logic and making heads or tails of the code.

The things I mentioned above are not the only tools at your disposal. You must learn and master the tools given to you by the programming language of your choice. Then, sit down and plan out your code/story. Place related elements of your code/story next to each other, at the right level of abstraction, so that the path which a reader should follow is clear. Even if it contains some flaws or bugs, once you’ve done this, you’ll end up with a code/story that can truly be desribed as… elegant.

This post is licensed under CC BY 4.0 by the author.