This is the 7th part of a series about 67 Bricks’s coding principles. The previous posts are: 1, 2, 3, 4, 5 and 6.
The principle
Aim to write code for the present and for your knowledge of the near future, rather than on assumptions about what might happen
If any of these principles are to be taken in moderation and with pinches of salt, it’s this one, so let’s start with a caveat. I’m not saying “don’t plan ahead” or “don’t consider how the codebase might change over time”. However, it is important not to code based on guesses or assumptions. That is, not to code “just in case”.
That’s the general point, but let’s be more specific:
Don’t write code that you don’t know you need
The common adage here is YAGNI: You Aren’t Gonna Need It. It can be tempting to build a feature because it seems likely that it will be useful in the future. However, there are various downsides to this. The first is simply that we might be wrong, in which case we’ve spent time and effort on something that isn’t useful rather than on something that is. That non-useful code will still require maintenance and add to the overall size and complexity of the codebase unless or until the decision is made to remove it again, incurring more time and effort – what Martin Fowler calls the “cost of carry”.
Even if it does turn out to be a useful feature, building it before understanding the full requirements is risky. It’s likely we made incorrect assumptions that will still require correction later. Plus we spent that time building the unknown thing that could have been spent on something with a more immediate priority.
Don’t optimise code prematurely
Optimising code for speed or memory consumption or any other measure often comes with trade-offs in terms of readability and maintainability – not to mention that it takes development time. So heed the very common advice to avoid premature optimisation.
This isn’t to say code should never be optimised. If you have identified a performance problem in some part of an application, you absolutely should track down the source of the issue and improve it. But you should be sure there is a problem to fix first.
This also isn’t to say you should entirely disregard performance while writing code. There is a sensible middle ground between caring too much and too little about things like performance. A good general rule is that there are likely more important features of your code to care about in the first instance, like readability, maintainability and testability.
Don’t generalise code prematurely
Generalising code for reuse is an extremely important tool in the toolbox of creating a scalable and maintainable application. However, not all code needs to be reused. There can be a temptation to assume that a general and reusable solution is automatically the best one. But if you don’t know whether it will ever need to be reused, then it’s often better to stick to solving exactly and only the problem at hand.
As with optimisation, there are often trade-offs with generalising or genericising a class or function. Often it makes the code more abstract and harder to read or to reason about. It’s also easy to make incorrect assumptions about the right way to genericise or abstract code if you try to do it too early. After all, duplication is far cheaper than the wrong abstraction.
One common adage around this is the Rule of three, which boils down to the advice that you should only try to make a reusable function if the need comes up three times. Before that you should just write code for the case or cases that you have.
This is an example where caveats abound. There are cases where the costs of reuse are low enough and the costs of duplication are high enough that it make sense to genericise earlier. Nevertheless, the Rule of Three is a good rule of thumb to keep in mind and to only break when you’re convinced you have a good reason.
Don’t keep unused code around
Sometimes a class or function may become vestigial – in that it is no longer called by anything in the codebase – perhaps following a refactor or the removal of a feature. There can be a temptation to leave that code in place regardless – after all, it’s good code that serves a purpose even if it’s not currently a necessary purpose. We could leave it just in case the need comes back.
This is generally a bad idea. Uncalled code adds further clutter to a codebase that may already be intimidatingly large for a new starter or for someone trying to find their way around. But worse, uncalled code still requires maintenance. That uncalled code likely relies on other functions and therefore acts as deadweight, artificially inflating how much those other functions are used and how easy they might be to refactor or change. Once again, the YAGNI principle applies.
The same goes for leaving commented-out code in place just in case. A commented out block is likely simply to become out of date as the codebase evolves around it and leaves it behind.
If you’re ever in the position of being tempted to leave some unused or commented out code in place, remember that (I hope!) you use git or some equivalent version control system. So that code can always be retrieved in the history if it’s needed. But let’s be honest it probably won’t be.
Of course this advice needs to be modified if you’re writing a code library where the intent is to provide a range of classes or functions to other developers. In that case parts of your public API may not be called by any other code in the project. But the advice still applies to any code that isn’t part of the public API of the library.
Ultimately there are costs and benefits associated with every choice we make when writing an application. It’s tempting to only see the benefits of “just in case” code – more functionality, more optimised code, more flexible code – so it’s important to be cognizant of the costs too: things like time, readability, maintainability.
There are times when those benefits will outweigh the costs and also times when they won’t. The most important thing is to make sure we’re fully aware of what costs and benefits we’re weighing when we make these choices and to be aware of what assumptions we’re making in the process.
There are times also when it’s beneficial to try harder than usual to predict the future or to err more on the side of “better safe than sorry”. Data storage decisions can be especially hard to change later for example. In cases where you’re acquiring data from another source or from user behaviour, if you haven’t been storing some piece of information that a future feature requires, you may never be able to go back and fill it in for historical records. So some reasonable assumptions about the future may need to be made here. What “reasonable” means is unfortunately the kind of question that requires experienced judgement.
There are difficult balances to strike here. It’s sensible to plan for unexpected changes of requirements and to design your codebase so that you keep your options open for the unknowns of the future. But that flexibility has to be balanced against the other more immediate concerns we keep coming back to – readability, maintainability, testability. Always examine your assumptions and try to take account of all the costs and benefits when weighing these decisions.