Dependencies
From APIDesign
Contents |
[edit] Dependencies
Not many APIs can live alone, without support from other parts of the system. Every C library needs libc, every Java code needs some version of JDK. As each library, also APIs have own environment, which defines what needs to be available around to allow the API to function properly. Each user of such shared library needs to recreate proper environment, that means to satisfy its dependencies, before its API can be used. This implies, that the dependencies of a library form an important API of the shared library itself.
These dependencies may not even be visible in external signatures (more about that in TypesOfDependencies)! They may only be needed during the runtime, internally, still changing them constitutes an API change. Imagine, that users of your API are using your library in some version and it works fine with just plain JDK. Suddenly, in newer release, you decide to change the library internals and depend on some other library, for example Jakarta Commons. That immediately means every user, who migrates to new version of your library, needs to include a version of Jakarta Commons in own application as well. This may or may not be a problem, however this is quite an externally visible change.
As the Chapter 3 defines APIs as everything that is externally visible, it makes sense to include shared library dependencies into the family of various types of APIs. In spite the fact, that it is very hidden kind of API, it is in fact one of the highest level kind and the API that we deal with the most during our day to day work.
[edit] Specifying Dependencies
Dependencies are usually specified as set of pairs. First element in each pair contains unique identification of needed API and also some specification of the API versions. Depending on the actual module system, the dependencies may be equality, greater than or even specified as a range.
It is important to mention that dependencies are almost always simplification of the real environment required. People often say: my library needs Java version 1.3, or version 1.5, while in fact then may mean: my application needs class JNDI (introduce as part of Java 1.3). Or my application requires annotations (added in version 1.5). That is more precise, however such exact description of own's dependencies is very complex and verbose. Much easier to simplify each dependency to just one number (with some interpretation).
[edit] Equality
One can depend on exact certain version of other library. This is however very restricting. If there is a part of application that needs Java 1.3 and another that needs Java 1.5, will you run each part of the application in different VM?
Thus this kind of dependency is useful only for modules which can be used separately, but are built and deployed together.
[edit] Greater Or Equal
For modules deployed individually one needs to allow certain freedom. The equality dependency is too restricting. Often people use equal or greater than dependency. This follows the usually accepted expectation that if program runs on Java 1.3, it will also run (relatively) fine on Java 1.5.
This of course requires the vendor of the library to follow rules of BackwardCompatibility - e.g. really design and especially evolve the API well. However most people designing commonly used libraries understand this and thus we can mostly rely on >= dependencies.
[edit] Need for Incompatible Version
In life of every API comes a moment when its provider needs to make an incompatible change. There is no way around it. Even Solaris, the most compatible software project I am aware of, needs to make an incompatible change in some published interface from time to time. Moreover BackwardCompatibility in Solaris is the most important mantra, in regular libraries, it may not be adhered to so strongly. Of course, it is better to find a way to provide AlternativeBehaviour, but sometimes there is really no backward compatible way around.
Then one needs to express the incompatibility in the versioning. Usually there is some major version number that, once increased means that the new version is no longer compatible with the previous one. This shall be used rarely, but each module system needs to provide some way of expressing incompatible versions.
Common way to implement this is to associate with each version an implicit upper bound. So when one writes some.library > = 1.4.3 one actually means any give me some library at least in version 1.4.3 but not 2.0 or higher. From here there is just a little step towards RangeDependencies.
[edit] RangeDependencies
Dependencies are important part of every Module system. Dependencies are here to abstract the complex relationship between a module and environment it needs for its own execution, compilation, linkage, etc. Dependencies are always a simplification of the real requirements, but that does not stop people from inventing new and new ways how to specify them more precisely.
It has already been discussed that dependency needs some lower bound (the moment when the needed API is first introduced) and also some higher bound (as incompatible changes are so often). How do we formalize it? Well, let's use some mathematics and describe dependencies as interval! As a result a dependency on any compatible version 1.4.3 and later, would be written as
.
This looks like a very good solution. It is also very rationalistic. It is reusing knowledge gained by mathematics in past and thus it cannot be wrong! Moreover it is general enough - one can use the same notation to express equality dependency by writing
. And also it is flexible as one can also depend on a smaller range of versions
. Last, but not least this solution also supports some level of cluelessness as it builds on something every programmer must have heart of during own school days.
No wonder that everyone one who wants to be seen as providing well designed, deeply thought out, powerful and theoretically backed solution will choose intervals. This is probably the reason why OSGi decided to use this kind of dependencies.
[edit] Too Much Flexibility will Kill You!
Although it may seem flexible, overusing RangeDependencies may lead to system that is too complex and almost no longer manageable. What traditionally happens only when people release incompatible versions (due to TransitivityOfIncompatibleChange) can suddenly happen in regular situations.
The proof claiming that managing module dependencies is NP-Complete operates with three types of modules:
- Mi which is present in two incompatible versions
- Fj which is present in three compatible versions
- a single module T
The claim is made that finding whether module T can be enabled is NP-Complete.
One of the most profound critiques however argues that it is wrong if three compatible versions of module Fj depend on incompatible versions of some other modules. This is valid observation, yet one can easily address it using RangeDependencies!
- instead of two incompatible versions of Mi define two compatible versions:
and
- use strict range for dependencies of Fj, so
depends on
or
depends on
- single module T continue to depend on classical range of
This scenario describes accepted practices of DistributedDevelopment. Imagine, there is one team providing various versions of module Mi. This team does its best to keep BackwardCompatibility. It is using versioning in the traditional way to denote compatible versions - e.g. the first major number stays the same, the second minor one is incremented with each new, compatible version.
Yet there is some other distant team producing module(s) Fj and this team (or its quality department) is afraid of signing blank checks and trusting the M-team to produce compatible versions. Thus they use restricted RangeDependency to require just subset of the versions (only those known during testing).
This is all that needs to happen. No incompatibilities, just a simple use of RangeDependencies and we are now facing an NP-Complete problem. This probably explains why there is 3SAT solver behind Equinox. Finding the right setup of modules depending on each other with RangeDependencies can be really hard. NP-Completely hard.

