Yesterday, during the lead up to the WWDC keynote, I started thinking about what really matters the most when it comes to software engineering. We (I include myself) tend to talk a lot about APIs, libraries, features, capabilities and tools as part of our work. And it often feels that making great code depends heavily on use of language features, and what's happening under the hood when our application, framework or library is running.
It's true these things are important, but when you look at the larger picture, I think you'll realize —as I did— that language features, optimizations, and how code is written all add up to no more than maybe 30% of great software.
So what is the other 70% of great software made up of? It's composed almost entirely of the conceptual solution that lives in the mind of the programmer. I don't just mean this in a pie-in-the-sky, "I just had a great idea for an app" way. I mean it in the deepest algorithmic sense of what should your code be doing at each step, with what pieces, and why.
Code is only as good as the thought behind it.
This may seem obvious to some, but in my experience the thought behind the code gets the least time and attention in far too many teams and projects. In fact, the ratios for time spent are probably reversed more often than not: only 30% of time is spent challenging and refining the conceptual solution, while 70% of the time is spend writing code, refining code and debugging code. And if you spend anywhere less than 50% of your time in your own project developing concepts instead of code, then it's time to reconsider priorities. Because no matter how optimized, clever and clean your code is, it will still only be as good as the thought behind it.
Let's consider, for example, writing an algorithm that helps a computer play poker. Given an initial hand of five cards, the algorithm should generate a decision on which cards to trade in for new ones in such a way as to maximize the probability of the computer getting a winning hand.
The solutions to this problem can cover a whole spectrum including:
- Pick between 0 and 3 cards randomly to exchange
- Keep any pairs, three-of-a-kinds, etc. and trade in whatever cards are left
- Have a massive dictionary of possible hands mapped to which cards should be traded in for each.
- Create a dictionary of valid hands and code that determines how "close" any given hand is to a valid hand. Select the target hand that is closest to the computer's current hand, and trade in the cards that are not part of the target hand.
Looking at the options listed above, it's clear that each solution will have drastically different strengths and weaknesses regardless of how the code is written or the language it's written in. In fact, each solution could be diagrammed out on a whiteboard without using any code at all. And any significant improvements to this algorithm in the future will not be because of code refactoring, new APIs or frameworks. They will be because the conceptual solution to the problem is evolved further and made more efficient and accurate in the mind of the developer (and possibly on a whiteboard). The code necessary to implement that better solution will follow.
Code is the Language, Not the Concept
It may help to think about this like our own human languages. A great novel or speech, translated into any language, will still be effective as long as the intent and ideas of the work were properly represented in each translated language. Worrying about the individual words matching up to the original work one by one, or what order they appear in, or punctuation don't really matter so much as:
- Clearly understanding the intent and concepts of the original work
- Assembling the target language in a way as to best express the intent and concepts
Similarly, programming languages are really just a way for a human to talk to a computer and explain to it what you want it to do. It is important how you frame your request for the computer to understand, but a good result is much MUCH more dependent on what you are requesting rather than how you are phrasing it.
So, what's the takeaway from all this? Simply this: let's spend more time challenging and iterating on the conceptual solutions we are implementing, rather than the semantics of how we implement them. As a start, run your solutions through the following questions:
- Does this solution accomplish the task in as few steps as possible?
- Does this solution avoid redundant steps, or having to do similar work at different points in the process?
- Does this solution solve only the one or two particular problems I'm working on, or is it general and abstract enough to solve an entire class of problems?
- Have you reduced this solution's necessary inputs and dependencies to the minimum number possible?
- Is the solution as nearly as possible stateless, meaning that it does not require outside invariants or configuration in order to function properly? The more stateless the solution, the more easily is can be used anywhere and the less unintended side effects are likely to result.
Code optimization is still important, as are readability and maintainability. But we should always focus first and primarily on what we are asking our code to do, and making that better.