Today, another small 2nd-order refactoring. I call it "wrap a Demeter". As with the others, this is a very modest step, but still quite useful in some situations.
Demeter violations are places where a client accesses not just an upstream service, but that service’s upstream services, or even that service’s service’s upstream services. It’s a common problem in evolving code, and left untended, a classic source of unchangeable code.
Demeter calls look this:
a is getting a B, but is using that B expressly to get a C, then finally calling some method on the C to do something.
(The getX()’s could as easily be exposed fields rather than methods: depends on idiom & language.)
I want to stave off some potential confusion. 1) You might be used to thinking that any chain of ‘.’s like these is just "fluent". 2) You might, alternatively, be thinking that any chain of ‘.’s like these is inherently evil. Neither of these assumptions is necessarily valid.
It might be, or it might not be. Fluency is a really confusing topic, but maybe it help to consider the two most common uses for it: in builders and in streaming chains.
In a builder, the return value for each of our methods between the .’s is the same. If our example could as easily be written a.getC().getB().doSomething(), that would be far more like the builder situation: the ordering of the operations is irrelevant.
In a stream-chain, otoh, the fluency is quite different, in that many/most of the methods actually coerce the type they’re called on. Stream operations like map or flatMap do this.
I’m going to upset you now: that’s usually a classic Demeter chain under fancy new terms.
Because stream-chains are so exciting for people, I am seeing a lot of them these days. Many that I see are both primitive obsession with a vengeance and Demeter violation with a vengeance.
The good news? Everything I’m saying about wrapping Demeter violations can be done equally as well on stream-chains as on the example I’m using as our base.
Second possible confusion: are all friend-of-a-friend calls inherently evil?
Naww. If my Single Responsibility is precisely to introduce you to my friend, or to actually allow strangers to navigate an object tree, well, what can I say: that’s my job, not a crime.
I will offer you an alternative way to understand it, tho: if my job is to let strangers navigate an object graph, then that should be my only job, and all the rest of the things we might do with such a graph, like making it or changing it, might better be done elsewhere.
What we’d be saying in that case: my job isn’t to reveal to you a concrete object hierarchy, it’s to expose an abstract object hierarchy while actually blocking visibility to the concrete objects I use to form it.
So anyway, why are Demeter chains a problem?
I said they are anti-change. Why? It’s simple: that sample chain is absolutely committed to every A having a B and every B having a C. And 1) everyone and 2) every client will be sharing that commitment.
If you have that starting snippet a hundred times repeated in your code, then on the day you realize that some B’s don’t have C’s, but an analogue (D) that can do an analogue to doSomething(), you’re going to have to find and change 100 lines of code.
And it can even get worse. Some of your clients don’t use exactly that snippet, but something ever so slightly different. No regex on earth will find them all.
As often happens, TDD and changeability are closely correlated. Demeter chains can be incredibly difficult to write microtests for. In fact, for noobs to TDD, they are the main initial pain-point.
If I write a client of a, and use that snippet, the only way to gain control over the client for testing is to have either a real or fake B and C. If real, I gotta load the B and the C with the right data. If fake, I gotta write mocks returning mocks.
Believe me, after the third time you write mocks returning mocks returning mocks, you’ll either see what I mean about demeters being a problem and learn how to wrap them or you’ll declare that TDD never works.
So what’s Demeter-wrapping?
It’s the dead simplest thing in the world. In our case, it means adding a doSomething() method to class A and giving it the body that is exactly the same as our snippet. It ‘s just that easy.
Now, clients of A depend on A. They don’t depend on B or C. They just know that A knows how to ‘doSomething()’.
Notice, once again, we’re not eliminating complexity, we’re isolating it. This is a huge recurring theme in the second chapter of learning to refactor, possibly the most important theme of that chapter.
Demeter-wrapping has an effect I want to start highlighting. (It applies to some of the prior refactorings as well, but I want to raise its profile starting now.) It gives us a new naming-point.
It is extremely common that our friend way down there, C, is more "primitive" — further from the domain, closer to the metal. That, after all, is why we use a chain to get to it, because we’re progressing down levels of abstraction.
We can take advantage of the new naming-point by actually changing the name from ‘doSomething()’ to, idunno, pick your domain, ‘adjustForGrossIncome()’, or anything else at the same conceptual level as A.
This is especially useful in stream-chains, where the 11 .-connected methods are usually 100% primitives around stream operation, and say nothing whatsoever about domain intent.
So, with a trivial refactoring, we 1) make clients have less code, 2) make clients easier to scan, 3) make clients easier to test, 4) improve our code’s direct representation of the domain.
It’s a dead simple step with powerful follow-on effects in terms of changeability.
I moved my rig out on the deck today, and it’s a beautiful spring afternoon. Even if I have to move it back inside tonight, it was a win.
I hope you all get a good win today, too. 🙂
The GeePaw Podcast
If you love the GeePaw Podcast, show your support with a monthly donation to help keep the content flowing. Support GeePaw Here. You can also show your support by sending in voice messages to be included in the podcasts. These can be questions, comments, etc. Submit Voice Message Here.