The Code We Don’t Write
The code we don’t write is just as important as the code we do write. As long as humans are doing the coding it’s worthwhile to pursue simplicity. Complexity grows when we add unnecessary abstractions in the pursuit of a mythical “pure Object-Oriented Design” ideal.
Attempting to code for potential future implementations is a prime source of code bloat. More generally we should favor less code as simply there is less that can break and if it does, it’s likely to be easier to debug.
Future Implementations
What will be the top news story one month from today? Who knows, right? There are way too many variables to predict.
Yet in software development, we are encouraged to write code to account for future implementations. This is generally a smart principle I think most would agree. However, it can easily lead us astray.
Be wary any time you find yourself writing code that is intended to account for potential future implementations.
It’s too easy to interpret this principle as giving us license to try and predict the future. Be wary any time you find yourself writing code that is intended to account for potential future implementations.
Unless you have long experience in the business domain, it’s likely you’re trying to code for things you can’t possibly predict with any significant degree of accuracy.
By all means, we should write our code with the ability to be extended! We should have the humility to not mistake this for assuming we can predict what that extended functionality will be.
Favor Just-In-Time Abstractions
Sometimes we forget, but almost every code base we work in will undergo significant change. In general, it’s much easier to add an abstraction when we need it than it is to remove one.
It’s human nature that we’re far more likely to leave an abstraction in place than we are to remove one. Especially if we’re not intimately familiar with the code and the potential ramifications removing it.
We should only add layers of abstraction with great care. Every layer of abstraction should be viewed as an inflection point of complexity within the system. We need to carefully assess whether the layer of abstraction will increase or decrease complexity and act accordingly.
Code to an Interface!?
It may be heretical, but we don’t need an interface for everything. This goes hand-in-hand with resisting the temptation to code for unknowable future implementations. In contrast, we should add abstractions and interfaces when there is a clear and immediate reason to do so.
Imagine we need to implement a simple BillingService for a new application. It’s a small company and there is only one billing system. Do we create a Java interface for BillingService? And then a BillingServiceImpl class implementing BillingService?
We find code like this everywhere. But what has the interface really given us here? Nothing, except some extra lines of code and a bunch of boilerplate that is essentially useless. Another file in the repository and more surface area for typos and bugs to potentially hide.
Another file in the repository and more surface area for typos and bugs to potentially hide.
In truth, when we write something like this we’re only doing it because we were told at some point to “code to an interface.” This is a useless abstraction here. The interface has bought us nothing, increased system complexity, and most perniciously made us feel like we have done the right thing because we adhered to “Good OOD” principles.
Just write BillingService as a class. If in the future our example company acquires another and we need to have two billing services, then we can refactor and have a good reason to do so. Then creating a Java interface will make more sense and the added layer of abstraction will buy us something worth having.
Counterpoint
To be fair though, a case can be made for writing BillingService as an interface and then a BillingServiceImpl as a class. The primary reason being most developers use this approach and will be instantly familiar with it.
That familiarity alone might be worth enough to consider using it regardless of its detriments. We’re all still using QWERTY keyboards after all, aren’t we?
Summary
In conclusion, the code we don’t write can add tremendous value by preventing cruft. Pursuing simplicity in our code is a worthwhile ideal and should take precedence over theoretical prescriptions such as “pure OOD.” In other words, good OOD is important, but not as important as simple, understandable code.
Be wary of trying to predict the future with your code. Future implementations are often unknowable and should be treated as such.
Adding abstractions to our code should be done with care. Add abstractions when they are needed, not when they might be needed if X, Y, and Z could become true in the future.
Coding to an interface is a good heuristic. However, this principle is often treated as a command and deployed mindlessly.
If you find yourself writing boilerplate interfaces, just stop for a moment and ask yourself what benefit it’s providing? Often enough, you’ll find you’re doing it out of habit rather than from conscious deliberation.