5
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.
FitNesse arrives in a manner unlike that of Junit or Ant or Spring (core) or of any of the programs examined so far in this series: its structure raises questions right from the outset.
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.
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 a()
→b()
, and b()
changes, this change could ripple back to a()
alone. If a monster like a()
→b()
→c()
→d()
→e()
→f()
→g()
→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 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.
Figure 5: FitNesse version 20130531.
Figure 5 shows the package structure of FitNesse in its latest guise. A heavily interconnected sprawl, its gangsters - the packages run
, responders
, fitnesse
and 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 Monkey
, Ape
and Orangutan
, another Proton
, Electron
and ProbabilityWave
. Providing access protection, however, relies entirely on syntax: the degree to which a package provides this protection can be derived 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 structural 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 structural 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.
The 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 structural 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, SlimService
and 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, WikiPage
and 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 Fixture
and 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.
The 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. Clean code this might be, clean structure it is not.
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.
Factor | Score |
Absolute potential couple efficiency % | 36 |
100 - (% length of longest dependency that half the system is shorter than) | 72 |
% Method transitive dependencies spanning 2 packages or fewer | 63 |
((25 - (number of transitive method dependencies per method) / 25) as % of 25 | 78.4 |
Average | 62% |
FitNesse 20130531's structural evaluation.