I had a discussion with a coworker the other day on the design of a certain piece of code. My goal was to add a new sub-service to a piece of software that this coworker had been the main architect of, and which I was not particularly familiar with. My initial proposal was to add a class with a specified API, and then instantiate and use an instance somewhere else in the code -- about as simple as I could get. Their response was that instead we should have a class which itself calls N different functions which will allow it to register various error-handling routines and the methods it exposes into some calling class. This calling class would then call the registered functions depending on what specification we register with it. The kicker was at the end of describing this approach they said:
This keeps the code decoupled.
Since they had "ownership" of this section of code, I did what they suggested, but this isn't about that. My initial reaction, which I kept to myself, was that this was the exact opposite of decoupled code! What they proposed, of course, is the design pattern called inversion of control. I consider Inversion of control a very powerful tool in certain situations -- but I've always considered it with the trade-off that the code would be more tightly coupled.
After our chat, I considered the situation further; in many ways the strategy they proposed did result in "decoupled" code for some interpretations of what "decoupled" code means. The "caller" class never knew what code it was running, as everything was registered at startup, which meant that it could make no assumptions about the various subservices and subroutines. Additionally, each "callee" class didn't know what other subservices or subroutines they could use unless they looked up the registered subservices at runtime, meaning that they had to handle failure to find something gracefully. However, this was very different from what I consider to be decoupled code since it tightly tied the "callee" to the "caller", meaning that we couldn't reuse the "callee" code in a different service without providing the exact same registration routines.
This got me thinking on what decoupled code even means. All software engineers know that decoupled code is good code, but its clear from this interaction that we might not agree on what decoupled code even is in the first place! I come from a C/C++ background, while my coworker comes from a Java background. Could this explain some of our differences? Very likely yes, and unfortunately I don't have the right background to defend their style, but I want to pitch why I believe my style results in "decoupled" code which is more useful.
The general approach I take in writing code is likely best described with a quick example. Consider "modules" A and B, where "module" could just be a function, with A "calling" B. In this situation, I want module B to know nothing about module A, and to instead expose a clear API with which A (or anyone else) can use. A, on the other hand, must know about module B, since it must call the correct APIs and to do this it must know what structures B expects. I try to minimize any "module" C which is used by both A and B, and generally restrict it to code which is basically "common knowledge" (such as HTTP codes or a very well-specified, and unchanging business rule).
The result of writing code like this is that module B can be called from anywhere or by any other module trivially. As the business progresses, module A might need to be written or re-factored, and this will not affect the implementation of module B whatsoever. Perhaps module B needs to be called from a new location -- this is not a problem! If module B needs to be refactored, then module A will need to change (and everything above), but this is not a problem if you approach the problem by keeping higher-level modules "light" (just shuttling around data-structures to various methods), while lower-level modules are allowed to be "heavier" (running algorithms, or specific business rules). This, from my perspective, is what decoupled code is all about.
There is a famous saying that medical students are taught information of which 50% is going to be out-of-date in a few years -- nobody knows which 50% though! In software I believe we have something similar, except its more like 100% of the code we write is going to be wrong -- we just don't know when! Keeping the various pieces of code easy to completely rewrite when needed helps keep tech-debt and legacy code down.
So what is decoupled code? And does its definition depend on the individual and their background? Of course I feel like "my way" is the correct way, but who knows if I'm right.