As a programmer that ships the occasional crash or nasty bug, I often find myself trying to find patterns of failure in what went wrong. Most programmers blame state for many of their issues, often times we simply forget about a possible scenario or it’s not clear in the first place.
However, I think state is just one facet of a bigger picture - implicit assumptions, or more simply put, requirements or contracts in code that are not explicitly stated.
These kinds of things pop up all over the place, some examples include:
- Global state - this is the classic example, often time you end up with global, shared state and many things that depend on it. Depending on the situation, trying to unravel all of this or infer what is needed to fix it is particularly tricky.
- Details not enforced or made clear by an API - this includes things like side effects from methods that aren’t documented well and often have other implications.
- Implicit schemas around data access, such as not documenting or enforcing what is nullable and what is not, or what possible values are accepted or returned in an API.
At first, a lot of these issues aren’t a big deal. Particularly on a smaller team where tribal knowledge can spread fairly quickly. Over time though, these contracts become more and more deeply ingrained in a code base and become very hard to refactor out safely.
Picking the right amount of implicitness
Removing implicit assumptions from a codebase is not necessarily the best decision. Too much implicitness and it’s hard to safely refactor or add new features. Too much explicitness can lead to a very rigid codebase that might be safe to work in, but changing it will be incredibly slow and cumbersome.
In my mind, there are a handful of different distinctions in how implicit a codebase is:
- Level 0: There are a bunch of implicit assumptions, but they seem fairly innocuous since the code base and/or team is fairly small.
- Level 1: As the code base grows what was manageable with Level 0 is now getting to be too much to keep in every developer’s head. Implicit assumptions are identified and documented.
- Level 2: Runtime checks are added to enforce implicit assumptions, usually in the form of unit or integration tests or other runtime checks like preconditions.
- Level 3: The API makes all the assumptions clear and enforces them at compile time.
I think more often, we need to strive for Level 3 from the start. Often times, a much more clear API can be designed if you just put a little bit of extra thought in to what you’re trying accomplish.
It’s up to you to determine what level you think is appropriate for your projects and correct course as time goes on. However, I find that being more cognizant about implicit contracts from the start allows you to understand the risk and course correct much earlier. Creating clear, simple, and correct APIs is much easier at the start of a project.