Iterating with NDepend to remove cyclic dependencies (Part II)
Published by Marco on
In the previous article, we discussed the task of Splitting up assemblies in Quino using NDepend. In this article, I’ll discuss both the high-level and low-level workflows I used with NDepend to efficiently clear up these cycles.
Please note that what follows is a description of how I have used the tool—so far—to get my very specific tasks accomplished. If you’re looking to solve other problems or want to solve the same problems more efficiently, you should take a look at the official NDepend documentation.
What were we doing?
To recap briefly: we are reducing dependencies among top-level namespaces in two large assemblies, in order to be able to split them up into multiple assemblies. The resulting assemblies will have dependencies on each other, but the idea is to make at least some parts of the Encodo/Quino libraries opt-in.
The plan of attack
On a high-level, I tackled the task in the following loosely defined phases.
- Remove direct, root-level dependencies
- This is the big first step—to get rid of the little black boxes. I made NDepend show only direct dependencies at first, to reduce clutter. More on specific techniques below.
- Remove indirect dependencies
- Crank up the magnification to show indirect dependencies as well. This will will help you root out the remaining cycles, which can be trickier if you’re not showing enough detail. On the contrary, if you turn on indirect dependencies too soon, you’ll be overwhelmed by darkness (see the depressing initial state of the Encodo assembly to the right).
- Examine dependencies between root-level namespaces
Even once you’ve gotten rid of all cycles, you may still have unwanted dependencies that hinder splitting namespaces into the desired constellation of assemblies.
For example, the plan is to split all logging and message-recording into an assembly called
Encodo.Logging. However, the
IRecorderinterface (with a single method,
Log()) is used practically everywhere. It quickly becomes necessary to split interfaces and implementation—with many more potential dependencies—into two assemblies for some very central interfaces and support classes. In this specific case, I moved
Even after you’ve conquered the black hole, you might still have quite a bit of work to do. Never fear, though: NDepend is there to help root out those dependencies as well.
- Examine cycles in non-root namespaces
Because we can split off smaller assemblies regardless, these dependencies are less important to clean up for our current purposes. However, once this code is packed into its own assembly, its namespaces become root namespaces of their own and—voila! you have more potentially nasty dependencies to deal with. Granted, the problem is less severe because you’re dealing with a logically smaller component.
In Quino, use non-root namespaces more for organization and less for defining components. Still, cycles are cycles and they’re worth examining and at least plucking the low-hanging fruit.
Removing root-level namespace cycles
With the high-level plan described above in hand, I repeated the following steps for the many dependencies I had to untangle. Don’t despair if it looks like your library has a ton of unwanted dependencies. If you’re smart about the ones you untangle first, you can make excellent—and, most importantly, rewarding—progress relatively quickly.
- Show the dependency matrix
- Choose the same assembly in the row and column
- Choose a square that’s black
- Click the name of the namespace in the column to show sub-namespaces
- Do the same in a row
- Keep zooming until you can see where there are dependencies that you don’t want
- Refactor/compile/run NDepend analysis to show changes
Once again, with pictures!
The high-level plan of attack sounded interesting, but might have left you cold with its abstraction. Then there was the promise of detail with a focus on root-level namespaces, but alas, you might still be left wondering just how exactly do you reduce these much-hated cycles?
I took some screenshots as I worked on Quino, to document my process and point out parts of NDepend I thought were eminently helpful.
Show only namespaces
I mentioned above that you should “[k]eep zooming in”, but how do you do that? A good first step is to zoom all the way out and show only direct namespace dependencies. This focuses only on
using references instead of the much-more frequent member accesses. In addition, I changed the default setting to show dependencies in only one direction—when a column references a row (blue), but not vice versa (green).
As you can see, the diagrams are considerably less busy than the one shown above. Here, we can see a few black spots that indicate cycles, but it’s not so many as to be overwhelming. You can hover over the offending squares to show more detail in a popup.
If you don’t see any more cycles between namespaces, switch the detail level to “Members”. Another very useful feature is to “Bind Matrix”, which forces the columns and rows to be shown in the same order and concentrates the cycles in a smaller area of the matrix.
As you can see in the diagram, NDepend then highlights the offending area and you can even click the upper-left corner to focus the matrix only on that particular cycle.
Drill down to classes
Once you’re looking at members, it isn’t enough to know just the namespaces involved—you need to know which types are referencing which types. The powerful matrix view lets you drill down through namespaces to show classes as well.
If your classes are large—another no-no, but one thing at a time—then you can drill down to show which method is calling which method to create the cycle. In the screenshot to the right, you can see where I had to do just that in order to finally figure out what was going on.
In that screenshot, you can also see something that I only discovered after using the tool for a while: the direction of usage is indicated with an arrow. You can turn off the tooltips—which are informative, but can be distracting for this task—and you don’t have to remember which color (blue or green) corresponds to which direction of usage.
On the right, you’ll see a diagram of all direct dependencies for the remaining area with a problem. You don’t see any black boxes, which means that all direct dependencies are gone. So we have to turn up the power of our microscope further to show indirect dependencies.
On the left, you can see that the scary, scary black hole from the start of our journey has been whittled down to a small, black spot. And that’s with all direct and indirect dependencies as well as both directions of usage turned on (i.e. the green boxes are back). This picture is much more pleasing, no?
Queries and graphs
For the last cluster of indirect dependencies shown above, I had to unpack another feature: NDepend queries: you can select any element and run a query to show using/used by assemblies/namespaces. The results are shown in a panel, where you can edit the query see live updates immediately.
Even with a highly zoomed-in view on the cycle, I still couldn’t see the problem, so I took NDepend’s suggestion and generated a graph of the final indirect dependency between
Expression). At this zoom level, the graph becomes more useful (for me) and illuminates problems that remain muddy in the matrix (see right).
Crossing the finish line
In order to finish the job efficiently, here are a handful of miscellaneous tips that are useful, but didn’t fit into the guide above.
- I set NDepend to automatically re-run an analysis on a successful build. The matrix updates automatically to reflect changes from the last analysis and won’t lose your place.
- If you have ReSharper, you’ll generally be able to tell whether you’ve fixed the dependencies because the usings will be grayed out in the offending file. You can make several fixes at once before rebuilding and rerunning the analysis.
- At higher zoom levels (e.g. having drilled down to methods), it is useful to toggle display of row dependencies back on because the dependency issue is only clear when you see the one green box in a sea of blue.
- Though Matrix Binding is useful for localizing, remember to toggle it off when you want to drill down in the row independently of the namespace selected in the column.
And BOOM! just like that, phase 1 (root namespaces) for Encodo was complete! Now, on to Quino.dll…
Depending on what shape your library is in, do not underestimate the work involved. Even with NDepend riding shotgun and barking out the course like a rally navigator, you still have to actually make the changes. That means lots of refactoring, lots of building, lots of analysis, lots of running tests and lots of reviews of at-times quite-sweeping changes to your code base. The destination is worth the journey, but do not embark on it lightly—and don’t forget to bring the right tools.