Your solution works, yes. Did you consider any other alternatives? Do you have a good reason for choosing your solution over those alternatives?
How many times have you been talking with others about a technical solution and have heard “it’s the only way” or “it’s the right way” used as a justification? If not, you’re one of the lucky ones. If you have, did it win the argument? Why do you think that is?
I was first exposed to this kind of thinking in the late 90s on slashdot.org. Mac vs. Windows was an ongoing religious war. There would be fiery, long-winded arguments, and the occasional random “Linux is the best!” comment. I’d spend time reading, imagining that I was objectively gathering all of the best arguments and I’d eventually come up with the ‘right’ answer.
That way of thinking followed me through school and even into my career in software. But it’s not right.
It’s wrong to think as if there is a single, objective right answer. Sure, in a very narrow space with a whole lot of very specific context and qualification, it might apply. Even
1+1 has more than one answer, based on the radix chosen: in base 2 the answer is 10, and in base 10 the answer is 2!
Once human behavior is brought into the equation, the options become infinite.
My first experience with the truth of infinite alternatives came in the form of my first programming class at Cal Poly, SLO. We were to turn in our assignments to a ‘robot’ which would run a battery of secret tests against it. If a submission failed you’d either get a plain ‘failed’ response or a cryptic set of test names, nothing more.
I struck up a rivalrous friendship with a few others in the class, and we’d compete to get the assignment done first. When we were all done, we’d sit back and hang out together. In that dorm room idleness, a friend had a compelling thought: “I wonder how different our implementations are?”
It was quite an idea. We had all passed what we imagined to be a large battery of tests which had been built up over the years. Given that highly rigorous constraint, how similar were our implementations? Since we were a bit afraid of sharing our code with each other, the first test was to write some new, larger tests to see if our applications continued to match output. Our first surprise was when our algorithms didn’t quite match at large scale. Was one of us wrong? Maybe, but we definitely all solved the problem at hand.
Later, as the class wound down, we finally shared our code. It was very, very different. In fact, it was so different that it was a pain to try to understand the other implementations. I didn’t fully dig in, aside from that initial feeling: “this is so foreign!”
Okay, so there are many, many solutions to any given problem. What’s the right way to think about that? We don’t have much practice, since this kind of attitude isn’t easy to teach: complete freedom in an assignment doesn’t guarantee that the target concept is exercised, and it makes grading a whole lot harder.
So, this is a skill we have to build up for ourselves.
A useful tool to help in generating alternatives is that of an axis. A continuum with an infinite number of options along it, with two interrelated concepts at each end.
The most classic of these is that of memory usage vs. CPU time. A given solution to a problem uses a certain amount of memory, and a certain number of processor instructions working with that data. These two concepts are on one continuum because you generally have to trade one for the other.
Sorting is a perfect example here. Most people immediately go to Quicksort as the ‘right’ solution. But Heapsort is guaranteed to take a maximum of
O(n log n) time at the cost of
O(n) memory usage, where Quicksort uses
O(log n) memory usage for
O(n log n) time in the average case, with possible
O(n^2) worst cases.
So, with Heapsort, you get better CPU time characteristics at the cost of more memory usage.
It’s all about the tradeoffs.
Memory usage vs. CPU time is just one of the axes we can use to think about potential sorting algorithms. We could consider other traits:
That’s a lot - and it’s very specific to sorting algorithms.
What about some other axes that might be interesting to software in general? Here’s a small list. Think about the code you’ve been working on lately - what would it look like at each extreme of these axes? What would it look like in the middle?
But wait a second. Are these really in just two dimensions? Is it always either/or?
Let’s consider Don’t Repeat Yourself (DRY) vs. Code locality: Having all the code for a given scenario in one place makes it easier to understand - no trips across the codebase, into some sort of
utilities directory, to figure out what’s going on. But, if you need to make one change to one common workflow across the entire app, you do want all code related to it in one place. You might forget to change one of the copies otherwise.
So how do we balance this? We need a way to get the benefits of both.
One answer is documentation. You don’t need to make a trip across the codebase and read the code if the documentation is readily available, trustworthy, and comprehensive.
None of these axes are truly as restrictive as they may suggest. Object-Oriented Design vs. Functional Design is only listed as a single axis because they are the two most common programming paradigms today. There are others.
Some of the axes are like a smooth slider, from one to the other. Others are like a switch in a couple positions. Others are just waiting to be transformed into a different type of control, with the application of your innovative new perspective. Can you break out of the box with your creative solution?
Programming is not easy, especially in high-pressure environments. The temptation is to use the first working solution that comes to mind. If the problem is very complex, this first solution can take long enough that you feel that you have no other choice. You think to yourself: “Any more time spent would be wasted!”
Absolutely not true! You know what’s wasteful? Writing code and tests, preparing a pull request, passing continuous integration, including others in code reviews, then needing to do it all over again because someone else spent the time to sit down and think about the problem, and so came up with a better solution.
You can avoid that by considering alternatives to the best of your ability, and including discussion of them in your pull request summary, or mentioning them during the code review. If there are yet more alternatives you haven’t considered, it’s a time for others to help you grow. If not, you can expand others’ minds while also keeping the conversation focused on your solution.
Think about every solution, every decision, as a collection of smaller decisions made across a collection of different types of controls, like a sound mixing board. Consider different positions for those controls, and be ready to have a discussion about every one of them.
This way of thinking is about more than just your code. You’ll notice a whole lot less high emotion during discussions if you and your team consider everything as a collection of tradeoffs. Now you have a vocabulary to talk about individual or team/organization preferences, and how that should affect your solutions.
Think in alternatives. And always be on the lookout for ways to break out the box, to find a better alternative entirely!