Having our đ° and đ˝ď¸ eating it too
Published . Estimated reading time: 8 minutes.
We have established that never changing anything leads to buildup of frustration, and that âmoving fast and breaking thingsâ is no better. Letâs seek a more reasonable middle ground.
Giving ourselves options
And I mean this literally: giving the user the option to pick the behaviour they want.
The appeal is obvious: if you need compatibility, you can get the old behaviour; if you want improved UX, you can get the new behaviour. But there are a lot of wrinkles to that.
First off, a simple question. What should the default behaviour be? Existing users want the old behaviour by default, so that the new version can be dropped in and their code keeps working; new users will want the new behaviour by default, otherwise having to opt into it is, itself, a papercut. This leave us exactly at our starting point, with the same tension; the only thing that has been gained is that the tension is reduced. Although, it can also be infuriating to realise that you encountered an obscure error merely because you missed a single paragraph in the manual⌠(yes, this example is exaggerated, since itâs part of a pastiche of programming languages)
The next problem is reasoning about behaviour. Because this choice necessarily implies that the behaviour of a particular piece also depends on the compatibility configuration. In particular, this hinders code reuse, since code copy-pasted from any help forum may not work as intended on your setup if itâs newerâor worse, itâs older!
Another problem is whether itâs possible to mix old and new behaviours in a single system; for example, you may have started your own work using more recent features, and eventually want to use a library which turns out to be incompatible with them.
And, lastly, this becomes a combinatorial explosion of old and new code paths, which increases the burden placed onto the maintainers and testers!
âThis store is closing down soonâ
Another way to soften the blow is to warn users in advance that the breakage is going to occur. This can be difficult to implement, depending on the kind of workflow you are normally providing: in more interactive contexts, in particular, it can be difficult to find a way to warn the user without breaking their flow and feeling like an annoyance. It can be even more difficult to describe what the new way is, too.
The effectiveness and usefulness of such a warning can be improved by providing guidance on how to switch from the old way to the new one (within reason, of course). Bonus points if some kind of automated tool exists that can perform the transformation automaticallyâusers will be happy if they donât have to do some kind of mechanical task by hand, particularly if they have a lot of code under their care!
Regardless, this solution has two flaws.
- First, if deprecations are too common, then users will tend to tune them out, and possibly outright disable deprecation warnings (and then complain when the breakage finally occurs).
- Further, how long should the deprecation period last? One month? Six months? A year? Five? Or maybe one release? Two? Until the next major version, maybe?
Unless -Wno-dev
is specified! Thatâs a useful flag for people who are just compiling CMake projects and find its output too verbose đ
Semantic Versioning
Well, speaking of version numbers, letâs talk about how to convey breakage to users, especially in a more gentle, more ahead-of-time manner.
The question âwhat goes into a version number?â is as old as software itself, and some folks have decided to formalise a methodology (one of many!) and call it SemVer (for âSemantic Versioningâ). The gist of SemVer is to split the version into three numbers, and the first one is changed when a backwards-incompatible change is made. Thus, going from version 2.3.6
to 2.3.11
or 2.5.1
should be safe, but you can expect something to seize if going to version 3.1.0
.
This is useful! Just looking at two version numbers, you can tell at a glance whether itâs safe to upgrade without giving second thought or attention. In fact, this can even be done by tools, such as Rustâs Cargo (see, for example, cargo update --breaking
). But itâs not a silver bullet either, because it turns out that what counts as breakage is not simple, sometimes astonishing even (scroll down a bit), and scarily often subjective..!
That said, SemVer adds an interesting provision that, essentially, âunder 0.x
anything goesâ. It was intended to allow âpre-productionâ testing not just in isolation, but itâs often abused because incrementing the major version number is scary. This is however achieving little to nothing, since users tend to generally disregard the first number, since itâs just a meaningless constantâI know at least one person who calls RGBDS 0.8.0 âRGBDS 8â. Please consider the wisdom from SemVer:
If you have a stable API on which users have come to depend, you should be 1.0.0. If youâre worrying a lot about backward compatibility, you should probably already be 1.0.0.
Unfortunately, this wisdom is ignored annoyingly often.
Version branches đł
Another way to ease the pain for users, is to keep maintaining a previous version after making an incompatible change. This can be considered a compile-time version of the âprogram optionsâ above: instead of the user making their choice at runtime, they select their options by selecting which version of the probgram they run.
Thus, after releasing 3.0.0, 2.6.2 will still be released; often, incorporating the changes from 3.0.1 or 3.1.0 (a process called âbackportingâ), perhaps only some of them.
Though, in that last part lies the major downside of version branches: the codebases necessarily diverge. This means two things. First, that they need to be developed, tested, etc. separatelyâwhich, like with the âruntime optionsâ above, generates extra effort on your maintainers. Second, that it may also require adapting the patches during the backporting process. This can introduce bugs (and thus reinforce the first point), and sometimes be difficult enough not to bother.
Conclusion
One last note: it is, further, possible to mix and match these techniques: for example, having a few runtime options, deprecating them, and removing them in a later version branch. See what works for your project!
Anyway; a common thread, perhaps the common thread, between each of these techniques, is that they involve far more work from the maintainer(s), if only to avoid quality slipping. Some projects, especially the smaller ones, may simply not have the resources that would need to be spent on such an endeavour.
The following paragraph is somewhat personal; please excuse my indulging in a little bit of venting. Small projects do not have many resources; especially when something is run by a few volunteers, they will usually prioritise what they enjoy working on. This is by design, since they have no binding obligations to any of their usersâthey are, after all, providing the fruit of their labour for free. Thus, harshly criticising them for their decisions, or otherwise throwing them under the bus, is not helpful. If anything, itâs the opposite, because youâll be either discouraging them, or alienating them (and thus theyâll grow to ignore any pertinent or constructive part of what you might be saying).