TDD pro-tip: demeter chains are anti-microtestable.
What is a demeter chain? It’s code where we talk to a collaborator’s collaborator. U can look up the origins of the name in your copious free time, but in regular code, it looks like this:
x = a.b.c.d.e.f; each one of those dots is a link in a demeter chain out of whatever the object is that contains that assignment. A is our direct collaborator. We might have been passed her, or given it in a constructor.
We might have new’d her, tho that’s another tip in the making. So objects of type a themselves have collaborators of type b. And b’s have c’s, and so on. In a demeter chain, we walk down that line looking for some value we need.
All even mildly interesting classes have collaborators, and most of those collaborators are themselves mildly interesting. And so on and so forth. "It’s turtles all the way down." Never mind the larger design questions, what it is the impact of a demeter chain on microtest testability?
Well, it’s simple. Microtests are all about taking control over the object. They completely control the input so they can know for sure the output and validate it. And they work cuz they’re cheap, fast, and plentiful.
For me to microtest code that includes
x = a.b.c.d.e.f, I have to control the value of x. That means I have to pass an a. But to make an a, I have to pass a b. And so on. Now, to make my supposedly cheap fast test, I have to create 5 objects and hook them up correctly.
SHARE HOW THAT MAKES ME FEEL! (hint: it makes me feel like it’s too much trouble to test my object.)
If I don’t write that test, tho, the question of whether my code does exactly what I meant it to do is left open. Things that are left open are things you have to bear in mind when you work near them. Things you have to bear in mind are mental bandwidth. We’ve powerful evidence that, as embodied humans, our mental bandwidth is sorely limited.
So do u get the causal connections here? Demeter chains lead to one of two choices: a) don’t test. B) test slowly and painfully. Either choice we make makes us slower to change layered branching logic.
A lot slower.
So. What do we do?
Well. As always in programming, there are a variety of answers, ranging from cheap hacks to deep re-design. Which one I use in a given situation is a judgment call based on a bunch of my specific contextual factors, like deadline pressure for instance.
The cheapest possible hack? In order to calculate x I need an object of type F, which i’m currently getting by chaining. What if I didn’t chain, but just made my calculation take an argument of type F?
Essentially, i’m passing the problem from being my problem to being my caller’s problem. After all, my caller had to give me that A object. She’s got just as much info as I have for getting to the F. Let her do it! That might not work so well in context — remember, there’s not one right way to get past demeter. There’s tons of them.
The right answer is just noticing the problem and doing something about it.
What’s another way? Well. We need that A for other stuff in the very same calculation, so we’re getting an A no matter what.
What if, instead of asking a friend of a friend of a friend of A, we just asked A? Give objects of type A the api to get the value of f for us. Our code looks like this:
x = a.f; now to test that code I just have to have a nice rigged A to give the answer I want. That’s way cheaper.
Now, if you do either of those things, or any of the other variety of changes that can kill off demeter, do you see what you did? You changed your design to make it more testable.
You exercised the steering premise: "Tests & testability are first-class participants in design." in the TDD world, anything that makes us not test the local branching logic or calculation we write is seen as a design problem.
Mastering TDD means a) learning to see the problems, c) learning the variety of ways each of those problems can be solved.