Used by software shops the world over, FitNesse is a highly successful integrated standalone wiki and acceptance testing framework. Offered here is a structural analysis of this fine product's source code.
Before we begin, the usual four caveats.
- Being a syntactic structural analysis, this review cares little for either program semantics or, no matter the exquisiteness, delivered user-experience.
- Structure merits investigation in and of itself because it governs the predictability of the potential cost of change. Poorly-structured systems exhibit excessive coupling in which ripple effects drastically blunt the precision of change-cost estimations. Well-structured systems may not necessarily be cheaper to maintain and upgrade but they generally spring fewer nasty surprises.
- This analysis depicts a package-structure as a spoiklin diagram in which a circle represents a package, a straight line represents a dependency from a package drawn above to one drawn below and a curved line represents a dependency from a package drawn below to one drawn above. The colour of a package indicates the relative number of transitive package dependencies of which it partakes: the redder, the more transitive dependencies.
- No diagram can prove structural value or cost. High-level analyses only ever prompt questions whose answers lie buried deep under geological layers of code.
All of history in a moment.
Figure 1: FitNesse version 20050731.
When first confronting an unfamiliar system's package structure the programmer faces an unvoiced but unavoidable challenge: do you understand the relationships on display here? Can you pick a package and quickly identify the shores over which ripple effects may wash when change strikes? Gazing at figure 1, few programmers perhaps would answer with glee.
Figure 1 shows the earliest incarnation of FitNesse available on the Maven repository. Given that all previous analyses began with modestly well-structured systems, we must assume that more ancient versions of FitNesse once existed but have since perished. This, however, presents our current analysis with a problem: where previous analyses attempted to sift from structural evolution the general trends that molded final forms, FitNesse sets off problematically and remains formless throughout its development. Its structure neither degrades nor improves. The problems that afflict it at its birth remain unsolved as it matures.
Instead of the usual examination of historical snapshots, therefore, this analysis will summarize FitNesse's structural evolution with some graphs before concentrating on its current state. Historians must satisfy themselves with the meager animation above.
Back to school.
Figure 2: System depth per release.
These graphs show the latest eight releases of four programs: FitNesse, Junit, Spring (core) and a random straggler.
Figure 2 presents the depth of each system, the average length of the transitive dependencies coursing through these programs. When it comes to dependencies, size matters. If method
a() depends on
b(), that is
b() changes, this change could ripple back to
a() alone. If a monster like
h() slithers into view then a change to any randomly chosen method can cascade causing all manner of expensive wreckage. The first is a transitive dependency of length 2, the latter, 8. With system depth being the average of these transitive dependency lengths, well-structured systems display low figures. The, "Shallower," the better. An average of 3 is fantastic. Anything below 6 is good. FitNesse, however, finds itself riding an unhappy thermal, spiraling ever upwards. The above
a()→ ... →
h() represents not an obscure outlier rarely encountered within the FitNesse source code but the average transitive dependency encountered. The above presents an unfortunate norm.
This, then, offers first evidence that the apparently jumbled package structure of FitNesse has not misled us. It would seem to reflect deeper, more systemic problems. Something's wrong.
Figure 3: Transitive dependencies per method per release.
Figure 3 shows another important aspect of coupling, namely the transitive dependency density, which is simply the number of transitive dependencies divided by the number of methods through which they rush. Measuring the number of paths that worm their way down into a program, the fewer a program has, the better. With a pachinko machine we pour metal ball bearings in at the top and try to predict their erratic fall. Similarly with programs: having 2000 methods joined by just 2000 transitive dependencies greatly eases the predicting of impacts and costs. A ratio this low, however, of one transitive dependency per method, takes terrific structural discipline. FitNesse's transitive dependencies oscillates around a casual figure of 5, and thus each method can expect to find itself involved in 5 transitive dependencies, generally higher than that of the other programs examined.
Figure 4: Circular dependencies per 1000 methods per release.
Figure 4 shows a more familiar measure, that of the method circular-dependency density, or the number of circular dependencies per 1000 methods. Circular dependencies need no introduction: they epitomize bad structure, clouding the prediction of impact costs difficult and leaving systems open to bizarre cost-overruns for seemingly trivial updates. FitNesse unfortunately leads its competitors for most of its available release history, though its last two releases have made impressive progress. In absolute terms, however, FitNesse still contains over a thousand circular dependencies.
(Poor Spring, despite having by far the best package structure yet encountered, has recently launched itself into a miserable orbit of circular dependencies which, though mostly confined to a single package, highlight the danger that can lurk even in well-illuminated structures.)
Finally, note that this analysis omitted release 20111025, a release which gorged itself on the entire Apache Velocity template engine only to spit it out unchewed the very next year. While held down, it bloated most of FitNesse's statistics horribly and so rendered the release something of an aberration.
The system today.
Figure 5: FitNesse version 20130531.
Figure 5 shows the package structure of FitNesse in its latest guise. Heavily interconnected, its prime offenders - the packages
wiki - have sewn up this joint as they bootleg coupling across lawless package boundaries. Tracing potential ripple effects seems a lost cause.
But perhaps a package-level view obscures a more considered sub-structure. Let us inspect some of the underlying class structures within the packages to see whether figure 5 portrays the system fairly.
A package exists to serve two essential purposes: to group related types and to provide access protection for its contents. The former entails semantics, that is, the meanings of the class names and correspondences with that which they represent. One package successfully grouping related types might contain classes
ProbabilityWave. Providing access protection, however, relies entirely on syntax: the degree to which a package provides this protection can be derived entirely from scrutinizing the contained classes and their relationships with those of other packages. Appraising this access protection thus appraises the coupling of the system as a whole, as where a package fails to provide access protection for its wards it exposes them to transitive dependencies reaching from other packages, supplying paths along which ripple effects may cascade and thereby weaving a coupling tapestry.
Of the sixty packages FitNesse comprises, the six largest - in terms of bytecode - shall be examined, a criterion not without criticism but whose choice rests on two grounds. Firstly, casting a net over the largest packages increases the probability that the sample statistically represents the system as a whole. Secondly, a programmer that takes care to minimise the coupling of large packages will probably take equal care with smaller ones which require less effort.
Of importance in the investigation of dependency management is the concept of abstraction and that modules should be as abstract as they are depended upon. Applied to Java, this produces two varieties of package: the implementation repository, composed mostly of implementation classes and the interface repository, of interfaces. (To avoid repetition, "Interface," and, "Abstract class," are considered synonymous.)
The tool used for this package investigation is the coupling expansion. This tool explodes the view of the classes within a package to include all those classes on which they transitively depend and all those classes which transitively depend on them. This graphically demonstrates the extent to which a package finds itself coupled to the rest of the system, the measure of which is simply the number of extra classes that an expansion reveals. If a package of twenty classes swells upon expansion to fifty then this package has provided poor access protection: here is the very signature of coupling. Not all packages are equal, however. Expanding an interface repository will naturally unearth the wealth of implementation classes which it serves. In an attempt to clarify this important distinction, implementation classes shall be coloured red and interfaces black. Thus where a predominance of red expands to yet more red, coupling lurks.
The coupling expansion is best shown by example, so figure 6 shows one of FitNesse's many well-structured packages before expansion, on the left, and after, on the right.
Figure 6: Package
fitness.schedule before (left) and after (right) expansion.
schedule package of figure 6 offers a shining example of a good structure. Unlike previous figures, each circle represents a class rather than a package, red being a concrete class, black an interface. The coupling expansion of this package adds a single interface,
Clock, and increases by just one the number of alternating grey and white horizontal bars, giving a rough estimate of the depth of the coupled ensemble. It is difficult to imagine any package being more loosely coupled within a system. Note also the clarity with which the classes depend on one another, promising an effortlessness of ripple-effect tracing which characterizes all good structure.
Figure 7: Package
fitness.slim.protocol before (left) and after (right) expansion.
Figure 7 gives an example of a package,
protocol, well-structured in isolation but less so in its wider context. Before expansion, on the left, it consists of just three classes, one of which, "
SlimSerializer, its name truncated," (as Picard might say), drifts independent of the other two. The expansion reveals a package both used by and a user of the classes in other implementation packages. Even the expanded version's dependencies, however, remain legible and crisp. Not all coupling is ruinous.
Now we come to the large packages.
Figure 8: Package
fitness.slim before (left) and after (right) expansion.
The first thing to be said about all these large packages is that, even before expansion, all seem a little too large: the smallest contains 34 implementation classes, the largest 84. That all have evaded the blades of refactoring somewhat surprises. The
slim package shown in figure 8 has 53 concrete classes whose intra-package coupling already raises concern,
StatementExecutor connecting to most of their colleagues. Post-expansion, 105 implementation classes have declared their interest in the package, though, on the positive side, the expansion does not reveal a great deepening: the transitive dependencies that surge through
slim seem at least theoretically tractable.
Figure 9: Package
fitness.wiki before (left) and after (right) expansion.
Figure 9 presents the
wiki package. Containing only 34 implementation classes before expansion, it unfortunately sports the highest gains thereafter, ballooning to a massive 198 implementation classes. Dark as that right-hand diagram seems, however, it shows that those two vast confluences of dependencies at least terminate on two of
wiki's 14 interfaces,
PageCrawler: this is to be applauded though it begs some questions. It suggests an interface repository as the primary role for the package, a role belied both by its housing 27 public concrete classes and by its transitive dependencies on 50 classes in other packages. Hope for this package lies in its being refactored into the implementation and interface repositories that it one day dreams of being.
Figure 10: Package
fit before (left) and after (right) expansion.
Figure 10 shows the 74 implementation classes of
fit and the two classes
TypeAdaptor firmly in command of a package which, if not exactly well-structured, at least strongly suggests to a programmer the (two) places from which any dependency stroll should begin. Where the pre-expansion diagram whispers of
Fixture's being more suited to an interface offering a fixture service than its current implementation class, the post-expansion diagram shouts. The package as a whole thus displays some of the ambiguity of its
wiki cousin: far more classes ripple into it than out from it, suggesting that it too may be more of an interface repository than its 74 implementation classes would have us believe.
Figure 11: Package
fitnesse.responders.run before (left) and after (right) expansion.
run package of figure 11 is well-structured when viewed alone. Despite the occasional mutual dependency, classes array themselves openly, offering little resistance to ripple-effect hunting. Its expansion then mashes together up a total of 187 implementation classes, this time with
run using more classes than it serves. Here, then, is no blushing interface repository;
run simply has too many fingers in too many concrete pies, its admirable intra-package structure dashed on the treacherous Coupling Crags.
Figure 12: Package
fitnesse.wikitext.parser before (left) and after (right) expansion.
Finally we come to figure 12's
parser package whose expansion rather speaks for itself.
As mentioned, many of FitNesse's other packages are well-structured and loosely coupled, yet even if the rest were all perfectly so the classes of the five largest packages shown above might still generate the majority of the coupling visible in the system's package structure in figure 5. The depth and density of the transitive dependencies that assail FitNesse justify a questioning of its package structure, a questioning that has not evaporated on deeper exploration of its class structures. That FitNesse can be considered a loosely coupled system seems doubtful.
Coupling is tricky; building well-structured systems, hard.