Software Development is about problem solving on a variety of levels of abstraction. From memory allocation, through function composition, to interaction design and usability, and about a million layers in between. We write software that is executed by machines to accomplish desired tasks; things like animating shapes across the screen of your mobile device, keeping a self-driving car in its lane, or ejecting a rocket booster at the appropriate altitude.
The machine is obviously important, but perhaps more so are the humans tasked with maintaining the executable instructions, of understanding their intended behavior. At some point in time, a human will need to reason about the code. The machines do exactly what we tell them to do, whether the instructions were our intention or not. The “best” developers are those who can write the simplest machine instructions to accomplish a given task, while also critically ensuring that the instructions are simple to reason about later in time for a human being (often the same individual).
You probably already know that the harder task is not the machine instructions, it’s the human component. As a beginner learning your first language, you can’t imagine what it’s like to effortlessly “think in the code”. Yet after years of experience, after learning and using additional languages, the task becomes “normal”. You can leverage the strengths of various languages without breaking a sweat.
But we still love to obsess over the machines, juicing every last bit of performance out of the smallest most obtuse piece of code we can manage. We say to ourselves, “Yes, I understand this one-liner, it’s very clever. I will definitely always know and understand what this thing is and why I wrote it like that.”
Except, of course, this is untrue. Our memories are provably horrible. How many times have you raged over a piece of code only to git blame and sheepishly read your name next to the offending lines. It happens. We’re fallible. We forget things. Admitting you have a problem is the first step to healing. It’s okay. We all do it.
And so we learn from our mistakes and we work really hard on the human part. We write code that accomplishes a task and is well designed from a human’s perspecitve. “No one,” we tell ourselves, “will ever be confused by this code. It is a bastion of clarity.” Other developers will review your code and agree, this could not be any simpler.
Yet all too often, time is money, and we don’t have the time to Make It Right. And so we do the best we can to solve the problem in a way that others will follow. Other developers will tell you, “We understand why you built what you built, and the complexity will have to do, because we have features to ship and more bugs in the bug lane.”
And so we move on. Plan. Estimate. Build. Ship. Test. Fix. Repeat. The (developer’s) circle of life.
Until one day it happens: you realize that you are missing a big, fat, giant, enormous step in this endless cycle of software development.
Did I ever stop and ask, “Is it useful?”
Oh sure, as an industry we talk about usability. UX is king, right? Except, (warning: controversial statement) nobody is doing UX. Everybody talks about doing UX, and we argue about button colors and css3 columns and font families and page views and a/b testing. But is the act of producing usable products built into the cyclical nature of our process? Do you have a team, or even a single developer, who is asking whether the thing they built is not only usable, but actually useful? Are you doing it consistently, on every feature and fix you produce?
On the hierarchy of needs for any application feature, Usefulness is Everest.
/\ / \ / \ / \ / Useful \ /----------\ / Usable \ /--------------\ / Beautiful \ /------------------\ / Working \ /----------------------\
Some definitions for you:
- Working: You got dressed this morning. Good job.
- Beautiful: You thought about the outfit and even did your hair all fancy.
- Usable: You built something that behaves well and is intuitive.
- Useful: You built something that improves someone’s life.
You may be thinking that I’m writing this because I recently screwed this up. You would be correct.
I just spent two months building a new feature into the Nuvi platform. I spent nearly a month gathering requirements, meeting with our Account Management team, having numerous phone calls with the client, holding numerous design sessions with product and engineering. The problem was an interesting one that took more time to plan than I had initially wagered. In the end, we all agreed and understood what it was that I would be building. And so off into the weeds I charged, mental model for the problem rooted firmly in my mind.
Building the feature took about a solid month of software development, in between other responsibilities I have. I produced a working feature. It looked good (thanks to our design team). It was intuitive to use. It would definitely improve the lives of our clients and internal teams.
I deployed the feature to stage. Various issues arose immediately due to testing with actual data, and I solved them quickly. I deployed the feature to production, protected behind a feature flag. Better data, more fixes. Three more pull requests and stage/prod deploys out of the way, and I’m done right? Wrong.
I tried to use the tool to accomplish its only task: easily determine which monitor’s velocity is growing the fastest. There weren’t any syntax issues, all my tests were passing, the icons were all aligned beautifully, the sorting and filtering worked exactly as expected. Yet, it was horribly obvious how incorrect the velocity calculations were. The simple algorithm was completely, utterly, and embarrasingly incorrect.
I had spent months discussing, designing, analyzing, writing tests, agonizing over class size and readability and composition and dependencies. How did I get it wrong? Why did it take so long to realize it was wrong? Conceptually fixing the issue was trivial, yet I had a non-trivial refactor on my hands if I wanted the code to be easier to reason about.
I am fascinated that I spent so much time thinking about and implementing a solution to this problem, writing tests for it, without realizing that my algorithm was not actually correct. I was, as an engineer, thinking about the problem from the code outwards, not the interface inwards. Only when I removed the code from the equation was I able to see how unuseful the solution was. Did the page “work”? Yes. Was it beautiful? Yes. Was it usable? Maybe. Was it useful? Definitely not. Thankfully, fixing the issue was about a day’s refactor and testing. Am I positive it’s useful now? No, not really, not until I get it in the hands of the client and ask for their feedback.
So next time you’re building That Thing, take some time to put yourself in the user’s shoes and just try to use the product. Ask yourself, is it Useful? You might be surprised what you find.