TransitivityOfIncompatibleChange
From APIDesign
TransitivityOfIncompatibleChange happens when reusing APIs provided by others as described in Chapter 10. However the whole concept is only meaningful when using semantic versioning - in case of general RangeDependencies, it looses its meaning.
Transitivity in Semantic World
Imagine you have a module Failed to parse (Can't write to or create math temp directory): M_{1.0}
which re-exports APIs provided by module Failed to parse (Can't write to or create math temp directory): N_{1.0}
. The re-exported APIs of module Failed to parse (Can't write to or create math temp directory): N
become part of the API of module Failed to parse (Can't write to or create math temp directory): M as they may be visible in Failed to parse (Can't write to or create math temp directory): M
's own class signatures as parameter or return types, or super types.
Let's now continue by envisioning that the author of module Failed to parse (Can't write to or create math temp directory): N
decides to release new, incompatible version. According to rules of semantic versioning the version should have different major version - e.g. let it be called Failed to parse (Can't write to or create math temp directory): N_{2.0}
.
Often, the producer of module Failed to parse (Can't write to or create math temp directory): M
would like to run on the most recent version of module Failed to parse (Can't write to or create math temp directory): N
. As such it may consider creating new version of own module which would depend on Failed to parse (Can't write to or create math temp directory): N_{2.0} . Can this version be called Failed to parse (Can't write to or create math temp directory): M_{1.1} ? Well, if we allow it to be called Failed to parse (Can't write to or create math temp directory): M_{1.1} , then:
- we run into NP-Complete problems as the LibraryReExportIsNPComplete proof shows
- we go against the motto of semantic versioning
Some parts of interface of Failed to parse (Can't write to or create math temp directory): N
are re-exported and thus basically part of the public signatures of Failed to parse (Can't write to or create math temp directory): M
. As there was an incompatible change in these interfaces, semantic versioning urges us to increment the major version of module Failed to parse (Can't write to or create math temp directory): M . The incompatible change in Failed to parse (Can't write to or create math temp directory): N
transitively influences Failed to parse (Can't write to or create math temp directory): M
. Its author should release Failed to parse (Can't write to or create math temp directory): M_{2.0} !
When Things Go Relative
The whole thing with transitivity gets quite complicated with use of RangeDependencies. There is no pre-defined meaning of various ranges and thus a general (and simple) advice of semantic versioning is not easily applicable to describe stability of APIs. It took me a while to grasp this difference and analyse it, but at the end it is not bad to give more power to end users:
How one specifies how stable just published API is? The semantic versioning suggests to treat all releases up to changed major version (e.g. up to 2.0.0) as being BinaryCompatible. The NetBeans API Stability proposal allows each API to be attributed as stable, devel or private. The netbeans:NbmPackageStability goes even further and suggests to use different ranges for different stability of the API. Is this the right approach?
In ideal, rationalistic world, it is. Under the assumption that API designers are rationalistic and understand the versioning issues and the importance of proper dependency management, it is fine to expect that they will do everything to follow principles of semantic versioning (or netbeans:NbmPackageStability guidelines) and either keep BackwardCompatibility or bump the major version. In rationalistic world, it is enough to believe our application will sort of work and at least link and the rest will be achieved by the good work of the other, rationalistic people.
The trouble is that we live in clueless world and most of the people (including API designers) don't have time to fully evaluate negative influence of their changes. When producers of up-stream library make a mistake, there should be a way for the consumers of such API to recover and shield themselves. From this thought there is just a small step to the idea of giving the users of an API right to classify stability of the API they use. Here the range dependencies come into the play.
Should there be a repository of modules and their dependencies including module Failed to parse (Can't write to or create math temp directory): M
in versions Failed to parse (Can't write to or create math temp directory): 1.1, 1.2, 1.5, 2.0, 2.5, 3.0
. If majority of other modules having a dependency on Failed to parse (Can't write to or create math temp directory): M
uses range Failed to parse (Can't write to or create math temp directory): [1.1,2.0) (e.g. the classical range for semantic versioning), then we can deduce that the owner of module Failed to parse (Can't write to or create math temp directory): M is doing good job keeping BackwardCompatibility. If many dependencies on Failed to parse (Can't write to or create math temp directory): M use for example Failed to parse (Can't write to or create math temp directory): [1.1,1.5)
, then we can deduce that something unnatural (from the point of semantic versioning happened in version Failed to parse (Can't write to or create math temp directory): 1.5 ) and that the owner of the module Failed to parse (Can't write to or create math temp directory): M
underestimated the impact of changes in version Failed to parse (Can't write to or create math temp directory): 1.5
. On the other hand, if many dependencies on Failed to parse (Can't write to or create math temp directory): M
use range like Failed to parse (Can't write to or create math temp directory): [1.1,3.0) then the incompatible change was probably not as incompatible as it might have seen.
The beauty of analysing the overall dependencies on a module is that it clearly expresses the statistical probability of BackwardCompatibility as seen by all users. It also allows the users to be sure application really runs as expected if the dependency is satisfied. Of course, the author of such API should set the expectations up by using stability category, but the ultimate decision remains in the hands of the API users.
Real vs. Ideal
While the TransitivityOfIncompatibleChange is a clearly understandable feature of semantic versioning, in real, clueless world we can get further by giving the ultimate power of deciding whether something is or is not compatible to users of API modules.
Expressing dependencies as ranges is an ideal tool to achive that. The meaning of transitivity then gets more fuzzy and less clear, but that may be a natural consequence of the complexity of real world module dependencies. Rather than impossing idealistic rules on API designers, it is more beneficial to give users of those APIs a right to vote (e.g. to select narrow or wide range).
Semantic versioning should still remain the basic (and ideal) approach for classification of incompatiblities. However, when there is an error in the StabilityOfAPI classification, there are ranges to help everyone to recover from it.