Developers face complex problems all day, every day.
We live in a complicated world, and it’s tempting to believe that every convoluted problem requires a Byzantine, multifaceted solution. In reality, while the world is problematic, it is governed by simple laws such as cause and effect, gravity, energy, and momentum.
We can take lessons from nature and apply them to code. Most of the time, simplicity leads to the most beautifully intricate outcomes.
‘Smart’ code is the wrong solution
It is easy enough to show off technical skill by adding hundreds of lines of code until it addresses every problem, follows Boolean logic down every conceivable rabbit hole, and inevitably turns into a mess.
The “smarter” the code, the more prone it is to bugs, the harder it is to debug, and the more difficult it is to extend and maintain. Working in this way also makes estimating the time needed to build and manage the code almost impossible.
Further, the lifespan of overly complex code tends to be very short. Dealing with the bugs and uncertainty that comes with this kind of code is frustrating, so developers wind up scrapping the solution altogether and building something else. This is a waste of time and money that could be spent on other projects.
Symptoms of code that is overly complex
How can you tell if your code needs to be simplified? Aside from the aforementioned general problems, there are five ways to identify overly complex code.
1. Functions take too many parameters
Too many moving parts are bad in physical machines, and it is just as problematic in code. Functions with more than two params (an important keyword in C#) are a red flag that your code is more complex than it needs to be.
2. Conditional logic is the dominant feature
“If” statements, switch statements, Boolean logic, ternary statements, and logical operators are all opportunities for code to fail when it comes up against an edge case. Too much of this conditional logic will often bring trouble with it.
3. Comments explaining the code
Good code is like a good joke. If you need to explain what is going on, it isn’t working.
4. Brittle code
Code that breaks often is a source of frustration instead of a useful tool. If tests are catching breakages you didn’t see, your code is too complicated.
5. Difficult to follow
There is nothing more annoying than working through a difficult patch of code and being interrupted halfway through, only to come back to your desk and realize you have completely lost your place. If it takes 30 minutes to figure out what you were working on, or if you can’t quickly explain your code to junior developers, this is a symptom that your code is trying to be overly smart, instead of stupidly brilliant.
5 principles for creating stupid code instead
There is a solution to all of these issues. Writing code that is dumbed down, or simple and efficient, allows your application to be smart and versatile. The more elegant the solution, the more polished the finished product will be.
Complicated code that gets added onto every time a problem arises starts to look like something that was designed by a committee. It is hard to use and hard to change. Instead, use the following five principles every time you start a new project, and you’ll be creating simple code that gets the job done every time.
1. Understand the problem
What is the purpose of the code you are writing? Truly understanding the core goals takes time and energy, but it can save you hours in creating your application, and will result in a much better end product.
To illustrate this point, consider a simple problem: Given the coordinates of two intersecting rectangles, write an algorithm to calculate the area of intersection. There are a myriad of ways to tackle this question and create an application that will answer it, but there’s a catch. The rectangles can be any size and in almost any position, so the edge cases can get away from you quickly.
However, if you step back and really think about the problem, you’ll realize a key truth: Between all eight coordinates, there are only four distinct X values, because the sides always line up. Only the ones in the middle matter for finding the area of the intersecting rectangle. It doesn’t matter which rectangle they are part of, only that they are in the middle.
So, if your code can sort the values for X, grab the two middle ones, and find the distance between those two, you have the length. Apply the same principle to the Y values, and you’ve got your answer. In this way, the code goes from a full page of tiny print to less than a dozen lines (with a helper function), without a single “if” statement.
Understanding and simplifying the problem is probably the most important step in creating stupidly brilliant code, so do not skip or skimp on this piece of the puzzle.
2. Decouple ideas
Anyone who went to school in computer science or IT knows about the single-responsibility principle. Functions work best when they are designed to do only one thing.
Apply this idea more broadly to everything in your code. Separate each outcome from the one next to it. UI, UI logic, styles, datasets, data translation, business logic, and third-party integrations are all different concerns, and should be kept separate from each other.
There are a variety of architectural patterns that discuss this and can be used to create the kind of simple, decoupled code that works every time. Minimum viable product, model-view-controller, model-view viewmodel, and other architectures and tools all address this problem when used properly.
3. Pass the thing instead of the parts to build the thing
There are more technical ways to describe this, but the wording, “Pass the thing instead of the parts to build the thing” stays with the spirit of being stupidly simple.
Boiled down, it means that instead of using multiple props and labels that are then assembled into a single package, you pass the end result: an image, link, or whatever it may be. This makes it easier to customize later, since the thing itself can be uploaded instead of trying to describe its parts in complicated lines of code.
Writers edit their work multiple times before they hit publish. Photographers take dozens of shots before landing on that perfect one. Athletes try different techniques and coaches to refine their signature style. Why should coding be any different?
The first draft of code is rarely a perfect fit. It’s fine to send it out if it gets the job done. But, when the opportunity presents itself, coming back and refactoring allows you to simplify the code and make it better than it was before.
Recognize that refactoring is an investment. It takes time away from other things and has to be accounted for in the budget. However, the payoff is always worth it in the end.
5. Test-driven development
Test-driven development (TDD) has been covered by lots of smart individuals. Robert C. Martin wrote a book called The Clean Coder that will tell you everything you need to know—but for the purposes of this article, I’ll give you the stupidly simple version.
Write one (and only one) failing test before you write any production code. Write just enough code to pass testing, then refactor, write another portion, and move on. Done correctly, TTD produces “dumb” code, since it forces you to follow the other four principles.
You can’t write good test-driven code if you don’t understand the problem. You have to separate the code, because you can’t test a function that’s doing 50 different things. It encourages passing the thing, and it forces you to refactor at each step along the way.
Get going on your stupid journey
These are not the only principles that help with writing stupidly brilliant applications, but they are a great place to start. Now go and write some dumb code!