This is the third part in this mini series whose goal is to show why static dependency analysis is largely useless.
Part 1 primarily used reasoning and essay-like arguments.
Part 2 provided hard data --- that is: actual dependency graphs --- showing that tangled packages (let alone tangled classes) are common in very successful libraries and tools.
This part will probably be much more entertaining: It shows the logical flaw underlying static dependency analysis.
So, here is a Java program with two classes. The code itself is silly. I just wanted to have a program with cyclic dependency that is doing something (no matter how
silly).
package p1;
import p2.B;
public class A {
private B b;
public void setB(B b) { this.b = b; }
public void f(String s) {
b.g(s);
}
}
package p2;
import p1.A;
public class B {
private A a;
public void setA(A a) { this.a = a; }
public void g(String s) {
if(s.length() != 0)
a.f(s.substring(1));
}
}
As expected, the dependency graph of packages p1, p2 is a cycle:
Now, let's rewrite this program by applying the following rules:
- All variables, parameters, fields, return types will have the type "Object"
- Every method invocation such as x.m(y,z) will be rewritten as: Util.call(x, "m", y, z). The Util class itself is shown in the fragment below.
(Reservation: I've intentionally left out rules for the handling of overloading, static members, and object creation as these are not needed for the example shown here)
Here is how the program looks like (to avoid confusion I now use packages pp1, pp2 rather than p1, p2).
package pp1;
import pp2.B;
import static pp0.Util.call;
public class A {
private Object b;
public void setB(Object b) { this.b = b; }
public void f(Object s) {
call(b, "g", s);
}
}
package pp2;
import static pp0.Util.call;
public class B {
private Object a;
public void setA(Object a) { this.a = a; }
public void g(Object s) {
if(!call(s, "length").equals(0))
call(a, "f", call(s, "substring", 1));
}
}
Of course, we also need to write the static method Util.call (in package pp0). Here it is:
package pp0;
import java.lang.reflect.Method;
public class Util {
public static Object call(Object r, String s,
Object... args) {
for(Method m : r.getClass().getMethods()) {
if(!m.getName().equals(s))
continue;
if(m.getParameterTypes().length != args.length)
continue;
try {
return m.invoke(r, args);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
}
We can now take a look at this dependency graph of pp0, pp1, pp2:
(Irony mode on) Wow! No tangles! This technique is awesome. No matter how big and tangled your program is, you can apply these two very simple transformation rules and you will get this nice-, loosely coupled-, code, where each class depends only on Util.call(). Amazing! (Irony mode off)
Do you see the paradox? Tools that detect cyclic-dependencies are using type information from your source code to help you find highly coupled modules. However, these tools encourage you to eradicate the information on which they rely. In a sense, they want you to make them blind. This makes no sense.
Clearly, the transformation rules that we applied did not really eliminate coupling. They just swept it under the rug. The coupling is still there, waiting for run-time to raise its head. If one claims that cyclic dependency analysis is not valid *after* the transformation, due to inability to determine the run-time dependencies, one should also acknowledge that cyclic dependency analysis was not valid *before* the transformation due to the same reason: inability to determine run-time dependencies.
Is there a morale here? I guess there is. Think about dynamically typed languages:
- If you believe in static analysis of dependencies you should start developing in a dynamically typed language and you will immediately boost your static analysis score through the roof: no cycles, no coupling of packages/classes/methods.
- (otherwise) you don't believe in static analysis. You know that the static picture is only a distorted reflection of the dynamic behavior. Hence, you know that you cannot trust a tool that tells you that your program is OK. If this is what you think, then you are already writing in Smalltalk, Self, Javascript or Ruby.
Update 1: Added "(Turning irony on)"
Update 2: Added the reservation