In my previous post on Re-factoring, Re-Structuring and the cost of Levelizing, I explained that increasing the value of the structure of a code base is less costly than expected. The point is to focus a while on Re-Structuring without changing any behavior (Re-Factoring). The biggest motivation for re-structuring code is to get rid of dependency cycles between your components (namespaces or assemblies). Once a code base is levelized (the proper term to say, once there are no more dependency cycles between your components), every refactoring task is naturally easier because you can always define clearly which levels of the code base are impacted.
With evolutionary design, you expect the design to evolve slowly over the course of the programming exercise. There’s no design at the beginning. You begin by coding a small amount of functionality, adding more functionality, and letting the design shift and shape.
One point is that it is not possible to apply evolutionary design without a solid suit of automatic test. The risk of introducing regressions bugs would be too high. This is why I understand automatic tests as a way to mirror and scaffold the behavior of a code base. Once the code base and its automatic tests are in-sync (i.e when all tests are green), they both define the same behavior. If the behavior of the code gets modified, some tests are not in-sync anymore with the code. If the behavior delta is a regression bug, the code needs to be aligned with tests. If the behavior delta is a new feature/requirement, the tests need to be aligned with the code.
The principle I would like to expose here is: Applying Evolutionary
Design is easier once the components dependency graph has no cycles. Levelized components are of course not a replacement for a solid test suit, it doesn’t deal with correctness and behavior. But the point of this blog post is to show that levelized components it is a good complement to unit test to apply evolutionary design.
The Need to define new Abstractions
In the evolutionary design discipline, amongst all refactoring motivations, I estimate that the need to define new suited abstractions is essential. Concretely, in evolutionary design you are not supposed to create an interface implemented by one only class. The class is used by the consumer code, and if in the future you’ll get the need to provide a second or more implementations, then it will be time to create the interface, its associated factory, and the logic to plug with the correct implementation. Typically fool me once, don’t fool me twice. In other words: I was not supposed to anticipate the need for an abstraction the first time, but I won’t hesitate to create it when I will face the need a second time.
Creating new Abstractions: the easy scenario
Creating one abstraction to abide by one simple new requirement is easy. It becomes problematic when a massive new functional requirement will have impacts a bit everywhere in your code base. You’ll need to create many interfaces to create what is named an abstract façade.
The typical case study is the need for some new RDMS. So far the application consumed only SQL Server data, but now it has also the need to consume Oracle data. This scenario is so typical that ADO.NET supports it out of the box. All the implementations to access a particular RDMS (what is named a data provider) can be encapsulated behind an abstract façade obtained from the System.data.Common.DBProviderFactory class. This data provider case study is seamless because the implementation of a data provider is cohesive and well decoupled from the rest of the .NET framework.
Creating new Abstractions: the real-world complex scenario
Let’s focus on a real-world example we had to face recently during the development of NDepend: the need was to let NDepend work on other platform than .NET, concretely Java first (the XDepend product) and then some others (C++ is in the pipe). The bulk of the code is platform agnostic and then, the idea makes sense in terms of Return over Investment. More precisely, the code specific to the .NET platform represents in terms of Lines of Code 11.8% of the whole code base and we can visualize it through the metric view:
But this view represents what we obtained once we regrouped in a dedicated assembly the code specific to .NET. This code is isolated from the rest of the code base behind an abstract façade. At the beginning, the code specific to .NET was spawned all over the code base. This included the code needed to analyze .NET assemblies, the CQL specific .NET
terminology (SELECT ASSEMBLIES…), the panel to let users define assemblies to analyze, the specific parsing of .NET coverage files, the Reflector and VisualStudio add-in, not to mention all the visual tool-tips and UI labels containing .NET specific vocabulary (IL code, assembly, attribute…). Concretely, in the metric view below, each blue rectangle represents a method/type/namespace/assembly that had some code specific to the .NET platform:
We were in the typical evolutionary design problematic: since the beginning we didn’t bother with the fact that NDepend might be used on another platform than .NET. Basically the new design we decided to put in place looked like this:
The fact that the code base was kept levelized since the beginning was a blessing to perform this massive re-structuring. That is what I am about to explain in 3 points.
First, considering the sub-components (i.e the namespaces) of the new .NET specific assemblies, they were naturally levelized. At this point there were no questions like who’s high level, who’s low level, who’s depend on what. All these questions were already implicitly answered in the original design because it didn’t contain dependencies cycles. There were no questions also about which partitioning code in components, they already existed in the original design.
Second, because the original design was levelized, the composition of interfaces in the abstract facade came naturally as a hierarchy. Here also the answer to questions like which interface present which feature and which property, were already contained in the original design.
Third, the abstract façade needed to remain at the bottom level. Every component use it but it cannot use anything else than tier code (like primitive types). Actually we had a problem here. We defined a library to represent some strong-typed file and directory paths: NDepend.Helpers.FileDirectoryPath.
We wanted the abstract façade able to expose such typed path objects. So the path library needed to be below the abstract facade. Fortunately, the path library didn’t use anything else than primitive types like string and char. This was not by chance but because the original code base was levelized. As roughly every components are using this path library, the path library has never been allowed to use anything else than tier code. Thus, placing the path library below the abstract façade was a matter of minutes. Dear reader, I am sure that you experienced in the past the joy of beging stuck many days to accomplish basically the same task on a real-world spaghetti/monster code base : rationalize dependencies to change the location of a component(s) in the overall structure of a code base (your best stories are welcomed in this post’s comments).
The lesson here is that keeping a code base levelized is an easy way to implicitly anticipate future requirements. Low-level components never get achance to bubble up in the architecture not because someone decided so, but because above components won’t let it bubble-up. Like in traditional building architecture, the structure itself put the pressure on low level components. In our example, the path library has never been allowed to use anything else than primitive types, not because we created a rule for that, but because it is roughly used everywhere in the code and we forbid dependencies cycles.
A nice consequence is that keeping a code base levelized discards the need for most design decisions. Good design is implicitly and continuously maintained. There are no questions about what to do to implement new requirement. When planning new code to implement the un-forecasted requirement, you just have to consider its fan-in/fan out (who will use this new code and who this new code will use). From this information and from the need to preserve levelization, you’ll infer the level and the right location where this new code needs to be added. Maybe you’ll need to use the injection of code or inversion of dependency patterns but only to preserve levelization, not because it seems cool to do so. And releases after releases, iterations after iterations, the design will evolve seamlessly toward something continuously flawless and unpredictable. Like in traditional building architecture, the structure won’t collapse.