Complexity Puts The Awesomeness In Your Apps!
In my current job, I work with a genius.
With a background in math, the way he reasons through complex problems is unparalleled. He creates carefully crafted algorithms that add awesome behavior to the apps we build.
My background, on the other hand, is in engineering.
Most of my experience comes from late nights coding and breaking my head over a missing semicolon in a Java program. Through those late nights, I have learned to keep code simple. Anytime when discussing something complex, my first instinct is to try to simplify it.
Last year, I wrote an article about making complexity pay rent. The article is mostly about the merit of deleting and refactoring complex code.
Lately, I’ve been thinking about the following question: when would we choose to add complexity to our code?
Complexity Is All Around Us
I flop down on the couch, grab my phone, and start watching Netflix.
The above sentence is literal magic. The phone, running the Unix kernel, handles process management, scheduling, file management, network management, memory management, etc. Each of those are incredibly complex areas in themselves. Starting a video on Netflix kicks off an encrypted load-balanced request to a server somewhere in the world. The server then streams the video to my phone, which decodes the video and displays it on my screen.
Each of the above subsystems are incredibly complex. But that complexity didn’t spawn out of nowhere. There’s a process of simpler solutions, gradually evolving into more effective solutions.
Complexity is something we choose when a simpler solution has proven effective, but there are gains to be made.
Complexity Is Fine In One-off Algorithms With a Clear Goal
Applying abstraction to reduce complexity is an investment in simpler and easier to maintain code.
But sometimes that investment just isn’t worth it.
“Maintainable code”, to a certain degree, is an illusion. The whole notion of “maintainable code” is centered around the idea that our code can adapt to a changing environment, and that certain ways of engineering are more resilient to that than others. While true, we usually still end up adapting existing code for a use-case or design that it originally wasn’t built for.
Instead, we should opt to delete code that doesn’t fit our environment anymore faster. And since deleted code doesn’t serve a purpose, its design is irrelevant.
As an example, let’s say we’re writing a very complex search algorithm. We start building a set of tests that cover every case. Then, we write one big 100-line complex function that covers the entire algorithm. The poor internal design of such a search should not matter. If it works, it works.
Then, when the environment changes, we first change the tests to reflect the new environment (in an ideal design, the tests reflect the environment, not the code). Then, we have two options:
- We throw away the original implementation, reconsider the design, and then re-implement. The reason why such a radical approach makes sense, is that small changes to the environment can reflect largely on what the expectations of the system are. Perhaps the old search just covered a very specific set of usecases, and given the new environment, a framework like Elasticsearch might make more sense.
- We factor out the logic that needs to change, then rebuild that from a blank slate.
Note that both of the above options do, mostly, not fiddle with the 100-line function. Changing that causes instability. Instead, we either make the problem bigger (what will the new system design be?) or smaller (what part of the logic addresses the changed environment?).
Not All Problems Have a Simple Solution
Coming back to the beginning of this article: through my simplistic way of approaching problems, I missed out on the solution space that the problem my colleague and I were solving was in. We were working on a problem to generate specific linguistic structures. My solution involved distilling wordsets into parts-of-speech, and then combining those to make sentences. My colleague used complicated NLP constructs to generate sentences. In the end, the solution my colleague derived performed much better.
The answer when it comes to “simplicity vs complexity” lies, as with all things, in the middle. Have too much complexity, and software will be buggy. But strive after too much simplicity, and you miss out on potential awesome features. Complexity can exist within a clean design, as long as a good testing strategy is applied.
My Duality With Complexity
I’ve realized there are three “stages” to dealing with complexity:
- Sometimes, I want to cry in a corner screaming “why is life so hard?” (complexity-averse mentality)
- At other times, I crack my knuckles, but on my power glove, and code the most amazing algorithms. (go-getter additude)
- And maybe, just maybe, I might actually read the documentation and figure out that everything’s not that complicated after all. (the “git gud” strategy)
Conclusion
Make complexity pay rent, but when it does, we might as well embrace it with open arms.
Cheers!