The Road to Quino 2.0: Maintaining architecture with NDepend (part II)
Published by marco on
In the previous article, I explained how we were using NDepend to clean up dependencies and the architecture of our Quino framework. You have to start somewhere, so I started with the two base assemblies: Quino and Encodo. Encodo only has dependencies on standard .NET assemblies, so let’s start with that one.
The first step in cleaning up the Encodo assembly is to remove dependencies on the Tools namespace. There seems to be some confusion as to what belongs in the Core namespace versus what belongs in the Tools namespace.
There are too many low-level classes and helpers in the Tools namespace. Just as a few examples, I moved the following classes from Tools to Core:
- BitTools
- ByteTools
- StringTools
- EnumerableTools
The names kind of speak for themselves: these classes clearly belong in a core component and not in a general collection of tools.
Now, how did I decide which elements to move to core? NDepend helped me visualize which classes are interdependent.
Direct Dependencies
We see that EnumerableTools
depends on StringTools
. I’d just moved EnumerableTools
to Encodo.Core
to reduce dependence on Encodo.Tools
. However, since StringTools
is still in the Tools
namespace, the dependency remains. This is how examining dependencies really helps clarify a design: it’s now totally obvious that something as low-level as StringTools
belongs in the Encodo.Core
namespace and not in the Encodo.Tools
namespace, which has everything but the kitchen sink in it.
Another example in the same vein is shown to the left, where we examine the dependencies of MessageTools
on Encodo.Tools
. The diagram explains that the colors correspond to the two dependency directions.[1]
We would like the Encodo.Messages
namespace to be independent of the Encodo.Tools
namespace, so we have to consider either (A) removing the references to ExceptionTools
and OperatingSystemTools
from MessageTools
or (B) moving those two dependencies to the Encodo.Core
namespace.
Choice (A) is unlikely while choice (B) beckons with the same logic as the example above: it’s now obvious that tools like ExceptionTools
and OperatingSystemTools
belong in Encodo.Core
rather than the kitchen-sink namespace.
Indirect Dependencies
Once you’re done cleaning up your direct dependencies, you still can’t just sit back on your laurels. Now, you’re ready to get started looking at indirect dependencies. These are dependencies that involve more than just two namespaces that use each other directly. NDepend displays these as red bounding blocks. The documentation indicates that these are probably good component boundaries, assuming that the dependencies are architecturally valid.
NDepend can only show you information about your code but can’t actually make the decisions for you. As we saw above, if you have what appear to be strange or unwanted dependencies, you have to decide how to fix them. In the cases above, it was obvious that certain code was just in the wrong namespace. In other cases, it may simply be a few bits of code are defined at too low a level.
Improper use of namespaces
For example, our standard practice for components is to put high-level concepts for the component at the Encodo.<ComponentName>
namespace. Then we would use those elements from sub-namespaces, like Encodo.<ComponentName>.Utils
. However, we also ended up placing types that then used that sub-namespace in the upper-level namespace, like ComponentNameTools.SetUpEnvironment()
or something like that. The call to SetUpEnvironment()
references the Utils
namespace which, in turn, references
the root namespace. This is a direct dependency, but if another namespace comes between, we have an indirect dependency.
This happens quite quickly for larger components, like Encodo.Security
.
The screenshots below show a high-level snapshot of the indirect dependencies in the Encodo assembly and then also a detail view, with all sub-namespaces expanded. The detail view is much larger but shows you much more information about the exact nature of the cycle. When you select a red bounding box, another panel shows the full details and exact nature of the dependency.
Base Camp Two: base library almost cleaned up
After a bunch of work, I’ve managed to reduce the dependencies to a set of interfaces that are clearly far too dependent on many subsystems.
- ICoreConfiguration: references configuration options for optional subsystems like the software updater, the login, the incident reporter and more
- ICoreFeedback: references feedbacks for several optional processes, like software-update, logins and more
- ICoreApplication: references both the core configuration and feedback
The white books for NDepend claim that “[t]echnically speaking, the task of merging the source code of several assemblies into one is a relatively light one that takes just a few hours.” However, this assumes that the code has already been properly separated into non-interdependent namespaces that correspond to components. These components can then relatively easily be extracted to separate assemblies.
The issue that I have above with the Encodo assembly is a thornier one: the interfaces themselves embody a pattern that is inherently non-decoupling. I need to change how the configuration and feedback work completely in order to decouple this code.
Roadmap for startup and configuration
To that end, I’ve created an issue in the issue-tracker for Quino, QNO-4659[2], titled “Re-examine how the configuration, feedback and application work together”. The design of these components predates our introduction of a service locator, which means it’s much more tightly coupled (as you can see above).
After some internal discussion, we’ve decided to change the design of the Encodo and Quino library support for application-level configuration and state.
- Merge the configuration and application
- To date, the configuration has contained all of the information necessary to run an application. The configuration was more-or-less stateless and corresponded to the definition of an application, akin to how a class is the underlying stateless definition, while an object is an instance of that definition. In practice, though, we always use a single application per configuration and the distinction is irrelevant, for all practical purposes. This will simplify all referencing code, as we will no longer need to pass around an
IApplication<TConfiguration, TFeedback>
. - Move the feedback to the service locator
- Instead of treating the feedback like a first-class citizen, with a direct reference on the application, make consumers use the service locator to retrieve an instance. This will remove the remaining generic argument in the definition of
IApplication
, leaving us with a base interface that is free of generic arguments. - Move specific configuration objects to the service locator
- The specific sub-interfaces that introduce dependencies are as follows:
- IncidentReporter
- SoftwareUpdater
- CommandSetManager
- LocationManager
- ConnectionSettingsManager
Any components that currently reference the properties on the
ICoreConfiguration
can use the service locator to retrieve an instance instead. - Move specific settings to sub-objects
- The configuration object is not only dependent on sub-objects, but is also overloaded with individual settings that are only used by very few specific sub-components. These will also be extracted into interfaces and moved into the service locator.
- ILoginConfiguration
- ISoftwareUpdateConfiguration
- IFileLogConfiguration
As you can see, while NDepend is indispensable for finding dependencies, it can—along with a good refactoring tool (we use ReSharper)—really only help you clean up the low-hanging fruit. While I started out trying to split assemblies, I’ve now been side-tracked into cleaning up an older and less–well-designed component—and that’s a very good thing.
There are some gnarly knots that will feel nearly unsolvable—but with a good amount of planning, those can be re-designed as well. As I mentioned in the previous article, though, we can do so only because we’re making a clean break from the 1.x version of Quino instead of trying to maintain backward compatibility.
It’s worth it, though: the new design already looks much cleaner and is much more easily explained to new developers. Once that rewrite is finished, the Encodo assembly should be clean and I’ll use NDepend to find good places to split up that rather large assembly into sensible sub-assemblies.
A
and B
are interdependent, but A
should not rely on B
, you should make sure A
is showing in the column. You can then examine dependencies on row B
—and then remove them. This works very nicely with both direct and indirect dependencies.↩