What they are, how to get rid of them
Functional dependencies are the bane of programming. They just cause problems: testability decreases, understandability decreases, function size increases the more functional dependencies proliferate. Hence in one word: Don’t! Don’t mess up your codebase with functional dependencies.
Here’s how they look in a nutshell:
There is some function containing logic (red area). Logic is the code where the action happens.
And at the same time the function is also calling another function (green area). That means, the calling function (CountDistinctWordsInFile) is depending on the called function (LoadTextFile).
If logic and calls to other functions are colocated in the same function, then that’s a functional dependency.
Because in essence not the containing function depends on the other function, but the logic. The logic only makes sense/can do its job, if the other function (or several of them) are called around the logic.
Why is this a problem?
For one, it’s a violation of the Single Level of Abstraction (SLA) principle. The logic is on a much lower level of abstraction than the line where the function is called. That makes understanding the composing function harder.
Most importantly, though, now the logic cannot be tested on its own. It can only be tested by calling the composing function which also calls another function whose logic then will also be tested. That makes reasoning about errors harder, that might lower the performance of the test.
And finally, mixing logic and calls to other functions rather sooner than later suggests to extract some logic into yet another function. That sounds great until it leads to bloated functions, because whenever you want to add more logic to a function, you can extract some. The result are functions with more and more logic interspersed with function calls. 100, 500, even 5000 lines of code in a function are quite common — and make reasoning and debugging of code very, very hard.
Here’s a visualization of this kind of evolution: A function starts out with just logic. Then more logic is added and some is extracted to another function. Then even more logic is added and more is extracted and so on…
There is no limit to how big functions can and will grow as long as functional dependencies are not considered harmful.
Commonly of these problems only one is attacked in a pragmatic way: the degradation of testability. The recommended remedy is Dependency Injection (DI).
Indeed this solves the problem of isolating the logic during testing: A surrogate can be injected which implements IPersistence and does only very little so that in case of a negative test result the source of the bug is limited to the logic (red area).
Unfortunately this approach leads to another kind of code bloat. In order to increase testability in all sorts of areas with functional dependencies, interfaces are added and dependency injection infrastructure is added. As Robert C. Martin himself said about this when he showed to what it leads with this image:
„Much of the complexity in that diagram was intended to make sure that the dependencies between the components pointed in the correct direction.“
Defusing functional dependencies by adding complexity! Seriously? To me that’s counterproductive advice. This graph I found on LinkedIn shows nicely what fighting functional dependencies with abstractions leads to. And fortunately it also shows, there’s hope beyond that:
To be able to precisely attach tests to logic — because logic is the part of code that’s hard to get right —, all functional dependencies have to be defused with DIP. That’s a lot of additional effort not clearing up a codebase but adding noise.
Logic interspersed with functional simply is a pain in the butt.
There has to be another solution!
And there is. The answer is simple:
Don’t try to defuse functional dependencies, but get rid of them altogether by applying the Integration Operation Segregation Principle (IOSP).
Is it really possible to have no functional dependencies as described/defined above in your codebase? Yes. Is it worth to strive for “zero functional dependencies”? Yes. Will you ever reach that point? No.
Even though you should really, really try hard to get rid of functional dependencies there’s always a trade-off. Sometimes the benefit does not warrant the effort. There are always some functions with some logic in them calling other functions where testability and understandability are not seriously impacted by the functional dependency. And that’s fine.
Nevertheless for most functions, probably 95%, functional dependencies should and can be get rid of by following the IOSP.