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:
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.
We see that
EnumerableTools depends on
StringTools. I'd just moved
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
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
MessageTools or (B) moving those two dependencies to the
Choice (A) is unlikely while choice (B) beckons with the same logic as the example above: it's now obvious that tools like
OperatingSystemTools belong in
Encodo.Core rather than the kitchen-sink namespace.
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.
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
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.
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.
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.
To that end, I've created an issue in the issue-tracker for Quino, QNO-46592, 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
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.
There is a setting to turn off showing the green dependencies -- where the row depends on the column -- to make it easier to read the matrix. If you do that, though, you have to make sure to select the class from which you're trying to remove dependencies in the column. For example, if class
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.↩
This link is to the Quino issue tracker, which requires a login.↩
Sign up for our Newsletter