Is Your Code Structured Like This?
JUnit's evolving structure.
JUnit is a masterpiece.
As Martin Fowler tells us, "JUnit was born on a flight from Zurich to the 1997 OOPSLA in Atlanta. Kent (Beck) was flying with Erich Gamma, and what else were two geeks to do on a long flight but program? The first version of JUnit was built there, pair programmed, and done test first."
It is rather difficult to think of any other piece of Java software that has made a greater contribution to Java software quality.
Kent Beck has done an equally noble service to our field by releasing the JUnit source code as it was written, thereby bequeathing us a permanent historical overview of its development.
Let us avail of this gesture. Let us take a rather superficial journey through JUnit's evolution by studying, not the enormous value it has brought or the lines of code which realised that value, but its package-structure.
We shall look at each release's package-structure as a spoiklin diagram in which a circle will represent a package, straight lines will represent dependencies from packages drawn above to those below and curved lines will represent dependencies from packages drawn below to those above. The colour of a package will indicate the relative number dependency tuples (that is, transitive dependencies) in which it partakes: the more red, the more dependency tuples.
We shall examine those releases most readily-available, from versions 3.7 up to 4.11.
Are we sitting comfortably? Then we'll begin.
The early phase.
Figure 1: JUnit version 3.7.
Figure 1 shows version 3.7 and perhaps one aspect seems most striking.
The diagram is lovely.
We can see at a glance, for example, that
awtui depends only on
framework. A confusable lot, programmers value such clarity, drawing from it a warm sense of no-overtimeness.
We should not, of course, read too much into a package-diagram. We do not know, for example, whether the
runner package holds five hundred or five million lines of code. Deeper investigation will show that no monsters lie hidden under beds here but we shall confine ourselves mostly to structure for the moment.
Structure being a set of elements and their dependencies, most would conclude that JUnit version 3.7 is a well-structured work. Let us move on to version 3.8
Figure 2: JUnit version 3.8.
In version 3.8 the
ui package has packed its bags but otherwise the structure is unchanged. If anything it appears even more aesthetically-pleasing than before; some might consider figure 2 a thing of beauty. Uglier constructions certainly besmirch half the tee-shirts that amble by you daily.
So far so good.
Figure 3: JUnit version 4.0 (version 3.9 was not in the repository).
Something has happened in version 4.0.
The diagram is still relatively clean-looking despite the number of packages having almost doubled from six to eleven. A nagging doubt has surfaced, however. In some qualitative way the elegance of version 3.8 has, if not been lost, at least suffered some tarnishing.
Curved lines have appeared indicating our first dependencies going up the page. In itself, this is insignificant; Spoiklin's algorithm produces such curves as artifacts rather than damning moral judgments.
A curved line only raises concern where it forms the infamous, "Bow," pattern of mutually-dependent elements and such, alas, is the case here. Mutually-dependent packages invite suspicion. Mutually-dependent packages suggest exposure to one another's ripple-effect changes and this troubles programmers.
Figure 3 reveals three such bows:
Still, the package-structure looks manageable and concerns are allayed with release 4.1 whose structure is identical with that of version 4.0. Let us move on to version 4.2.
Figure 4: JUnit version 4.2.
Version 4.2 is also similar to 4.0; a single new package is added,
internal. The bows that arrived with version 4.0 have failed to metastasize, structural degradation appears arrested.
Version 4.3, however, is growling around the corner.
The middle phase.
Figure 5: JUnit version 4.3.
Version 4.3 represents by far the largest structural change in the history studied here. Beneath the surface, the number of functions leaps from 564 in version 4.2 to 1309 in version 4.4 (it will fall almost as dramatically in the next release), though the number of packages rises from eleven to just sixteen. (This non-commensurate rise in the number of packages may help explain the fall in configuration efficiency from 31% to just 18%.)
The introduction of the
tests packages seems the most significant change yet the authors have performed this introduction quite successfully. A good test of a structure is the ease with which one can point to any element and identify the other elements it depends on directly and transitively. Though there are many,
tests's dependencies are readily traceable.
Also, we note that the number of bows remains unchanged: the surge of functions has not triggered an increase in mutually-dependent packages.
Figure 6: JUnit version 4.4.
Figure 6 shows version 4.4 and
tests leaves us before we really got to know it. The number of functions falls from 1309 to 853; the number of packages rises to 19.
Among the new packages to make their entrance are the
hamcrest cluster (bottom right) which also unfortunately bring three more mutually-dependent packages to the party. Still, this cluster shies from the main group. We may have some difficulty counting the number of bows in that main group (has it increased by 1?) but a diligent programmer could still hope to identify all transitive dependencies from a randomly chosen target.
This is perhaps the last time that such is the case.
The late phase.
Figure 7: JUnit version 4.5.
A difficulty in tracing transitive dependencies characterizes the late phase of JUnit's package-structure.
Figure 7 shows version 4.5. 150 functions more than version 4.4, the system boasts a configuration efficiency risen from 29% to 34%. Its twenty-six packages, however, have become embroiled.
For the first time, a programmer introduced to this system might pause before accepting the assignment. No doubt the drawing-algorithm generating the diagram must take some blame: a better algorithm might do a better job. No algorithm, however, can veil the brash rise in inter-connectedness on display.
hamcrest, previously to one side, has gravitated towards the big players, tugged by new dependencies from both
internal. It will remain center-stage until its sudden elimination in version 4.11.
How many mutually-dependent packages do we have? Who can say? If we change a package, can we easily predict which clients will be affected? Can say with any certainty which clients will not be affected?
Such questions dog all later revisions so let us skip forward to the last: all intermediates are visible in the animated graphic at the top.
Figure 8: JUnit version 4.11.
By version 4.11, transitive dependencies have proliferated seemingly unchecked. We are far from the short dependency-chains and few cyclic-dependencies of good structure.
And this ultimately is the point.
A software's user-experience realization is just another term for what that software does. This user-experience realization and its structure are, however, practically orthogonal. Perhaps hundreds of thousands of programmers use JUnit daily. It is a phenomenally well-tested piece of software and it works beautifully. Yet this tells us nothing about its structure.
Structure is not about execution: it's about development cost. The greater the inter-connectedness of a structure, the more ripple-effects can occur during updates and the more costly those updates can grow.
You cannot test good structure into a design. JUnit exemplifies this: it was written test-first but its structure has nevertheless degraded significantly with time.
Testing - at least unit- and acceptance-testing as we generally know them - is about user-experience realization. We write unit tests such as, "Verify that bank account balance does not fall below zero." We do not write unit tests such as, "Verify that package A has no dependencies on package B, C, D or Z." Yet the latter is precisely the type of consideration we must make when we create structure.
There is another way.
It doesn't have to be like this.
We can build package-structures that scale well with program size.
There are many ways to do this, but one way is to practice radial encapsulation. Figure 9 shows the evolution of a radially-encapsulated program that is far bigger than JUnit yet has throughout its history retained a structural clarity that JUnit seems to have abandoned.
Figure 9: A radially-encapsulated program (end configuration efficiency=69%).
Programmers should be forced to wear their systems' package-structures on their tee-shirts.
Practice radial encapsulation.
JUnit is a masterpiece.