There have been some NDepend users’
requests to make the tool
able to cop with dynamically fabricated dependency/IoC framework
such as Spring.NET.
The idea is to read configuration files to artificially inject these dependencies
into the code model fabricated by NDepend. As every relevant user requests we put
this feature in our TODO list but I would like to explain here why it is not a
top priority feature.
Quick remind on abstractions
Why do we use abstraction
in our code? There are plenty of ways to define abstractions like
interface, generic, delegate, base class/polymorphism , IoC through reflection,
C#4 dynamics… :
// Interface
class FooUser1 {
void MethodUser(IFoo foo) { foo.Method(); }
}
// Generic
class FooUser2<T> where T : IFoo {
void MethodUser(T foo) { foo.Method(); }
}
// Delegate
internal delegate void MyDelegate();
class FooUser3 {
void MethodUser(MyDelegate fooMethod) { fooMethod(); }
}
…
100% of our code could
be rewritten with C style code without any abstraction. The reason why we use
abstraction is to make our source code more understandable and as a direct
consequence, more maintainable. Abstraction is a mean to isolate implementations
(i.e method bodies). If the MethodA() receive an object through an interface,
when calling a MethodB() on this interface, the developer of the MethodA() cannot
know anything about the class(es) that implement
the MethodB().
From the MethodA()
perspective, the MethodB() implementation is something abstract. The benefit
is that the developer in charge of writing the implementation(s) of MethodB(), can refactor
it more easily, because its caller(s), like MethodA(), are not supposed to rely
on some implementation details. In practice, this works pretty well and
abstractions bring welcomed flexibility to make our code more refactorable, which
leads to more maintainable program, which leads to more agility, which leads to
more productivity, which leads to less cost.
Static vs. Dynamic view: Abstraction and Dependency
If we look at abstraction
through the dependency prism, we can see that it helps reduce the complexity of
the method/class call graph.
// Without abstraction
void MethodA(Foo foo) {
if(…) {
foo.MethodB_Impl1();
} else if(…) {
foo.MethodB_Impl2();
}
…
} else if(…) {
foo.MethodB_ImplN();
}
}
Thanks to the interface IFoo
and the polymorphism, MethodA() can depend on one abstract MethodB() instead of
N concrete MethodB() implementations.
This shows clearly that
there are 2 call graphs to consider. The one that can be inferred from the
source code, i.e calls between methods in the source code, and the one that can
be inferred by looking at calls between method implementations at run-time. It
makes sense to qualify the first graph with the term static, and the second one with the term dynamic.
This example also shows
that the dynamic dependencies graph is more complex than the static dependencies
graph. This comes naturally because the static dependencies graph is a
sub-graph of the dynamic one. Without surprise, abstraction helps decreasing
complexity.
Static structure is the key
The idea I would like to
defend now is that when it comes to
understand and maintain a program, one need to focus mostly on the static dependencies, the ones found in
the source code. Knowing dynamic
dependencies (who calls who at runtime) can make sense for example to profile performance
or to fix some particular bugs. But most of the time invested to understand a
program is generally spent in browsing static
dependencies.
What does it mean to understand some code? IMHO understanding
some code means to be able to recover the mental state of the developer(s) who did
the code (maybe it was you a few months or a few years ago). This way you’ll be
able to refactor the code and to extend its features to abide by new functional
requirements.
There are really 2 levels
of understandability: the micro-level,
i.e understanding methods bodies and their immediate interactions, and the macro-level, i.e understanding why and how
types/namespaces/assemblies, depend on each other. While tools like Resharper
help dealing with the micro-level,
tools like NDepend help
dealing with the macro-level.
The key is that
if you
need to know what implementation resides behind an abstraction, you are
in trouble: not only there might be multiple different implementations
but also,
some of these implementations might be created or refactored in the
future. Of
course, we cheat sometime by using the debugger to see at run-time
which
implementation(s) is really used behind an abstraction. But compared to
the show me static usage of this code element
action, using the debugger is more the exception than the rule, precisely,
because the intention of the
developer that created the abstraction is to prevent the need for knowing
implementation details.
The need for Loose-Coupling vs. the need for Big
Picture
Let’s now conclude on the
initial problem: NDepend should be able to inject Spring.NET dynamic
dependencies into its static dependencies
model of the code base. One worked hard to achieve loose-coupling between some
components through the advanced concept of IoC. Now one gets all the benefit of
abstraction. One is able to understand and extend features of smaller components
without bothering much with other components implementations. While it makes
sense to get the dynamic big picture of
how loose-coupled components depend on each other at run-time (for some configurations),
this NDepend added-value seems smaller than being able to focus on the static structures of components
themselves.