I know what you’re thinking: I’m one of those people who could find a user interface in a paper bag—someone who sees user interfaces everywhere. You would be correct on that point. Please hear me out anyway.
I’ve had two big themes in my software development career: writing better code and understanding usability. Using more marketable terminology, I’ll call those “clean coding” and “the principles of good user-interface design.”
I was surprised when it occurred to me that those two things were connected.
Specifically, the principles that apply to designing intuitive, understandable user interfaces also apply to writing high-quality, understandable code. Isn’t that the coolest thing ever? ヾ(*´ ∇ `)ノ
Write, design, and code for humans
Writing clean code is really about writing code that other humans can understand and work with.
Good user-interface design reduces an application’s learning curve and gives users meaningful feedback about their interactions. It allows people to complete the tasks they came to do quickly and efficiently. The best interfaces let users figure out how to accomplish a task intuitively—with little or no training—because the controls and menus are where they expect them to be.
In other words, it is about making the interface usable for humans.
The same ideas apply to code.
Reduce cognitive load
Good interface design and well-written code both reduce cognitive load. Humans have working memory a little like the RAM in a computer. It is temporary, short-term memory that you use to accomplish everyday tasks such as writing an email or having a conversation.
And just like computer memory, there is a limit to how much working memory you can use at once.
If too much information comes at you at the same time, you start dropping some of it because you simply cannot hold it all. That can feel like being overwhelmed, forgetful, or like your brain hurts.
Ever hear somebody say “this code makes my head hurt” about confusing code?
There is a good chance the code presents too many details at once. That gets worse when complex arrangements are layered on top of it. That arrow function inside an arrow function inside an arrow function seemed clever at the time, didn’t it?
If we can write code that does not require mental gymnastics to understand, we reduce the cognitive load on our fellow developers.
Function extraction: an example
One of my favorite techniques from the Clean Code books involves extracting pieces of code into well-named functions.
I once wrote a JavaScript switch statement that allowed other developers to set a notification in our core application. The notification system had different levels—info, error, and so on. I could have left the switch statement in the middle of the code that used it. Instead, I extracted it into a function called `setLevel`.
A few hours later, I realized that name was as clear as mud and renamed it `setNotificationLevel`. Then I moved the function and its switch statement to the bottom of the file, away from the more important code.
Other developers no longer had to read the entire switch statement to understand what the surrounding code was doing, because as the author I had communicated what I believed it did. They could dig into the details if they needed to by scrolling down to the function.
The switch statement was not especially hard to understand, but it contained a lot of detailed logic—ors, ands, nots, and so forth—that mattered only when somebody needed to debug that particular code. Otherwise, those details occupied working memory unnecessarily and made it a little harder to understand the part of the codebase that actually mattered.
Hide unnecessary details
Good interfaces do the same thing: they hide details users do not need, or are unlikely to need very often. They might put a less-frequently used feature, such as updating account information, in an out-of-the-way menu.
They also hide implementation details. When you search Google, you do not need to understand the inner workings of its search algorithm in order to search the internet effectively.
That is the point of a user interface: giving people a way to accomplish the task they came for without bogging them down in information they do not need.
Guideposts
Good interfaces also provide just enough guidance in the right place and at the right time. Think about the arrows on a side-scrolling carousel, a step component that shows where you are in a workflow, or a tooltip that draws your attention to what you should do next.
Can we provide simple guideposts in our code to help other developers find their way around?
That is where good naming comes in. My function name both pointed to the implementation and communicated what I intended the function to do.
Likewise, how we organize code into files and folders can guide fellow developers. On the frontend, the way we organize HTML into components—and even how we use CSS classes—affects how easy the code is to navigate and understand.
Reading flow
A good interface considers how somebody reads it: top to bottom, left to right, right to left, with a screen reader, or with another device.
Notice how many submit and save buttons appear at the lower-right side of a screen, where the eyes of left-to-right readers naturally arrive as they scan a page.
That suggests another useful code-writing technique: put the most important code in a file near the top and the small extracted implementation functions near the bottom. Fellow developers can ignore the details when they do not need them and quickly find the most relevant functions in the document.
Design patterns
The last principle I want to cover is the use of recognizable, reusable design patterns.
The classic example is the play-and-pause controls for video and sound. You know what that little right-pointing triangle means whenever you see it. A designer who invents a completely different symbol for play would confuse users while passing up an extremely powerful tool: a familiar pattern that most people understand immediately.
Are there recognizable patterns in code that allow fellow developers to infer what we are trying to do?
Yes. There are many volumes of books on that exact topic. I am going to skirt right past the myriad object-oriented examples and point out that we often use patterns most effectively in our tests.
Every codebase I have worked on had a consistent style for its test suites that made it clear what a test was testing and how it was set up. The patterns differ across languages. Python unit tests, for example, have a different style from most JavaScript unit tests I have seen.
I was surprised to discover that Python developers who had not worked with JavaScript testing frameworks such as Mocha and Jest found them confusing, while they instantly understood the patterns in other Python developers’ tests. As a JavaScript developer, I had assumed my tests were self-explanatory. ¯\_(ツ)_/¯
UI principles recap
Here are the principles I mentioned for reducing cognitive load in code and interfaces:
- Hide details the user does not need.
- Provide just enough guidance in the right place and at the right time.
- Understand how the user’s eyes or assistive technology will read the interface, and put the next steps within that flow.
- Use recognizable, reusable design patterns.
There are many more: spacing and style consistency—tabs or spaces—preventing errors, keeping things simple, and providing useful feedback for an interaction. Think about how tests provide useful feedback about code. We can apply all of those principles to code as well.
There are many ways that treating code as the user interface it is can help us write better software.
Just as interfaces are designed to be used by our fellow humans, well-written code is designed to be read and understood by humans. The servers and browsers that ultimately run the code are almost a secondary concern. If it does not work correctly, it will be ourselves or another human who has to read through it, debug it, and fix it.
Who reads the code
I once had an IT support person troubleshoot my code effectively without ever having written a line of JavaScript. It was a relatively simple application, but it was still very cool that the little codebase was understandable to somebody who was technically proficient but not a developer.
That is what I aim for in code quality. If QA or support can get a pretty good idea of what the code does just by reading it, I know I have done a reasonably good job writing it.