You’ve probably seen this. Ugly code. Unmaintainable, scary eight-thousanders (aka god classes). Joyful SOA (aka microservices) architectures with clear and beautiful diagrams drawn by senior-high-class-ivory-tower-what-not-“architects”. Diagrams as clear and beautiful, as bogus and worthless (haven’t you noticed that all these diagrams tend to have three to five perfectly aligned rectangles connected with arrows, all idyllically pointing downwards?).
Chances are you’ve seen this. And chances are you’ve seen this, because someone, at some point of time, crossed the magic line of loose coupling and made a step into the tightly coupled world. A world, which you definitely want run away from. Unless your self-preservation instinct is failing.
Hold on, hold on (few words on logical coupling)
OK. Not every occurrence of high coupling is necessarily a bad thing. But let’s start from the beginning and let’s define, what a coupling is. And let’s agree on a fairly simple definition. This is a metric that determines a degree, to which two elements depend upon each other. Coupling = dependency. We will say that two elements are loosely coupled, when the dependency between these two elements is reduced to minimum.
On the logical level, coupling can be observed separately for outgoing dependencies (class O calls classes A, B and C) and incoming dependencies (class I is called by classes A, B and C). In case, you’d like to appear smarter than you really are, you can use these fancy terms for such simple concepts: efferent coupling (often referred to as Ce) and afferent coupling (Ca) accordingly. Slightly less respectful names would be: fan-out and fan-in. Let’s just call them: coupling-out (= fan-out = efferent coupling) and coupling-in (= fan-in = afferent coupling).
In case of pure numerical measurement, coupling can be low or high. In case of its quality – it can be loose or tight.
So when is it OK, to have high coupling? Let’s say you are using some library class. Like a String. Or a Logger. You should expect to have rather high values in terms of coupling-in for these types. Lots of other classes should be coupled to them. And… that’s about it. I guess you could come up with one or two more examples, in which high/tight coupling is desirable. But – that would be it.
High/tight logical coupling – mostly bad
Let’s go back to the case of poor-quality-code. Its coupling will be most probably high/tight. Chances are that this high/tight coupling will be accompanied by the violation of others principles as well. Like Single Responsibility Principle. A class with high coupling-out may be one of these god classes. Which will definitely violate SRP, having (as Uncle Bob likes to say) more than one reason to change.
A class which exposes lots of getters and setters, simply asking for tight coupling-in, will lack proper encapsulation. Exposing getters and setters is really like asking for troubles. Expose them, and stay assured that people will call them in the least expected way. Maybe you did it by accident, it’s this ALT+INSERT/Getter and Setter in IntelliJ which you call almost automatically. Or you needed them for DTO mapping (but why did you make them public in such case?). Doesn’t matter. You just don’t set a ball on a dog (have a look here). A Dog should be a class with appropriate behaviour. It can take a ball from you, you don’t usually set it on a dog, don’t you?
Using objects as data structures does not count. It is not OO. Don’t ask for troubles, don’t allow for such silly coupling-in to play a mischievous joke on you.
Composition vs inheritance
You know, that there is some reasoning behind this favour composition over inheritance rule you’ve once heard. Yes, this one also brings coupling to the table. Unless you’ve found a suitable application of a template method design pattern (plus one or two other cases), you’re probably overusing inheritance. Especially when it’s more than one level deep. That’s asking for trouble in the area of inheritance. This is tight coupling of a child class to its ancestors. And this abuse may bite back as well. When the time comes to make a change in the root of the hierarchy – you will find it almost impossible. What you will discover will be in fact a rigid code. And please, do not solve this problem by adding an applySpecialLogic boolean flag in the overloading method. Impact? Unknown. Maintainability? None. You will learn very soon that there was another case you’ve forgotten to cover. And another one. Next release. Another discovery. Next release. Oh no, another one? And what did you expect making your inheritance chain four levels deep with no apparent reason? It’s very often a case that before you start fixing a bug or adding a feature, you should really make some effort to understand the code you’re working with and redesign it in a way to actually make some space for a change.
Dependency Inversion Principle
Speaking of the applySpecialLogic boolean flag. It will go in the next if statement, right? Just like another if statement you add for a new contract type. Or a new invoice type. Or a new payroll policy. Coupling high level policies with low level details. Coupling lots of classes together. And this is definitely not healthy either. And it happens to violate another two principles. Open-Closed Principle: our code should be open for extensions, and not pure modifications of the source code. And Dependency Inversion Principle: a high level policy should not depend on a low level detail. Wouldn’t it be better to use the most exciting benefit that Object Oriented programming offers? Convenient polymorphism. Wouldn’t it be better to apply let’s say a strategy pattern and make a high level policy dependent on a carefully crafted abstraction, instead of all these nasty low level details? And make low level details also dependent on the same abstraction, thus inverting the flow of control? Runtime dependency remains untouched, but dependencies on the source code level now point in the opposite direction. Making your code much easier to read and understand.
Another, this time even more entangled, tight coupling of a high level policy with low level details. A pseudo polymorphic call. It was once polymorphic, and now it’s not. Because someone had to make a quick fix. One, small, innocent if statement breaking the whole concept: instanceof. Expanding the list of violated principles with another violation. Liskov Substitution Principle: a client calling a base class must not know differences between its derivatives.
Sometimes, coupling can be little more abstract. Let’s say, you have few methods scattered all over your eight-thousander. Or throughout few random classes (at least you cannot find any logical reasoning behind such design) mostly named like …Util or …Manager. And all of these methods accept strikingly similar list of arguments. And these arguments (often in a twisted order) make these methods tightly coupled, with no obvious indication that some concept is missing here. One which could be expressed as a class with all these common arguments represented as private fields. I don’t have to add how hard it is to maintain such code, do I?
System design level
There is one more case, which also makes an inglorious representative of hidden coupling, which is even more dangerous due to its expansiveness. And to discuss it, we have to zoom out a little, and have a look at our system from the design or even (for lack of a better term, I will use this one) architecture point of view. It’s when you decide to extract a separate service (according to the latest fuss, this is not a service anymore, it is a microservice), and you start to wonder what you should do with this database. Or you just use the same one right away. Because I already have this data here, don’t I? It would be such a waste to migrate part of it. And even if I did it – how would I run all my reports…?
So you did it. You made a separate service using the same, good-old, three-hundred-tables-large database. Bad news: you did not make a separate service. You made the worst possible thing. You’ve just added another middleware (possibly together with a front end) on top of the same database. But now, you can’t just press ALT+F7 in IntelliJ/Netbeans or CTRL+SHIFT+G in Eclipse to find all usages of some type. Now you have two separate projects, developed (probably) by two separate teams. Finding all usages is nothing close to a simple task. You’ve just tightly coupled both (so called) services through the old database. Now, whatever change you make to your database with one system in mind, rest assured that it will break the other system. And this is the worst possible hidden coupling you can introduce – coupling to the same, shared resource. Database, in this case. Untraceable, because it’s hidden. And boy oh boy, it’s hidden well, behind all of the layers of your architecture.
And yes – making it work in a different way, with split databases, does require a change in a mindset, another techniques. Rethink your system boundaries. Focus more on the transactional requirements: which attributes of the same entity need to be modified in an atomic fashion. Group them together in a single service. Apply publish/subscribe messaging style to populate you new service with necessary data when it is changed at the source. Apply single source of truth principle. Work more closely with your Product Owner. Since now you have your services split according to carefully crafted transactional boundaries, chances are that your reports should be adjusted to accommodate this split. If that is not possible, why don’t you apply pub-sub one more time and collect all your reports specific knowledge in a separate service. Not optimal, but with such solution you might still focus on business requirements in your core services while allowing yourself for a little more mess in the reporting one (if you really can’t escape from having cross-boundaries reports). Lots of possibilities. Lots of solutions. One prerequisite: a change in a mindset.
Apart from the hidden coupling to a shared resource (like a database), there are more types of coupling which can be discussed on the system design level. Suffice to say that both of logical types of coupling apply here. A service might be coupled in or out with respect to some other service in the same way, as a class is used by or depends on some other class. Maybe a little less obvious type of coupling visible on the systems level is however temporal coupling. In this type of coupling, when a service A calls a service B, and it is a time consuming process that needs to be executed in the service B, the service A is waiting. Which means, that it’s temporarily coupled to the service B. This simple observation makes all (so popular) REST callouts at least worth giving a second thought. As with the second fallacy of distributed computing (latency is zero), it is worth realising, that calling another service does actually take some time. And very often you might not have any control over what the other service is doing and how well it is optimised performance wise. Which constitutes another strong argument for having another look in how you are designing dependencies between your services. And yes, publish-subscribe messaging style might be an answer.
Even within a single service, there is a way to very effectively decouple particular areas of business logic. Like sending a confirmation e-mail after finalising a purchase. This can be decoupled in a pub-sub manner, but applied on a different level – by using observer pattern. And this is particularly easy in JEE. You might want to have a look here to see how easy it is to start playing with it.
This technique can be very useful to implement the following principle in handling aggregates in DDD. That only a single aggregate should be modified per a transaction. Just remember, that by default all occurrences of @Observes are handled within the same transaction in which event producer is running. In order to make the processing of @Observes work in a new transaction, you have to add @Asynchronous annotation. Have a look at this blog post to find a very nice in-depth elaboration on the topic.
And again – new approaches are required. Unlike a straightforward spaghetti code, business logic decoupled with observer pattern (with async observers) does require new ways of error handling. You won’t be able to inform a user about all problems right away. Some logic is now invoked outside of the main loop of user interaction. But maybe this really is not a problem? Maybe you have invested too much time in your error handling anyway? How probable is a situation, in which your database runs out of a tablespace? So why did you prepare a separate message just for this case? Your operation failed? Try again in five minutes. Failed three times in a row? Report it in some sort of an error queue. Use monitoring and prepare KPIs dashboard showing key parameters of your system, a health check. And logging like crazy won’t help you much. Going through Java stack traces is far from enjoyable. So be terse.
Whatever reporting or logging solution you choose, just don’t mix these auxiliary (yet still very important) topics with your business logic, which should always remain clean, uncluttered and readable. And what if it turns out that your observers need to communicate as hell? You either ruined your design, or this observer pattern is simply not for you. Then forget it. Find a solution that works. Just remember to make it clean, uncluttered and readable.
There is no such thing as a free lunch. And also keeping your code and design loosely coupled comes with some cost. You have to educate yourself in new areas, master design patterns and SOLID principles, broaden your intellectual horizons. Even seemingly trivial things, like getting to know your IDE really well, are of great importance. You have to be able to easily navigate through code, find usages of classes and interfaces, browse hierarchies. There won’t be a four-hundred-lines-of-code method in front of you anymore, which you could browse through gritted teeth and (more or less, mostly less) know what the code is doing. You have to be able to grasp a bigger picture. Good news is that a high quality code makes it possible. You can now consciously choose the level on which you are reading and writing the code. Be it a high level policy or a low level detail.
On the system design level, this means most often mastering seemingly counterintuitive solutions. Plus, addressing many mundane issues which will pop up. Like difficulties in tracking down errors, especially those reported in the production system (to which you will most of the time have no insight). So make sure, you have your logging done right. Have a single, unique id stuck in every possible log statement throughout the whole use case, whichever (micro)service is involved. Rethink or introduce proper monitoring. Make KPIs dashboard to allow for a quick, visual diagnosis of your system.
Coupling is a roomy term. There are lots of types of coupling. It is present on every possible level of software development, from the source code to the architecture. It can be low or high, loose or tight – depending on whether you aim to express it as a number of logical dependencies or their overall quality. Most of the time, when being high or tight, it tends to be accompanied by violation of at least one of SOLID principles. However, we cannot forget about all these cases, in which high/tight coupling is more than welcome and it would be a pointer of a code smell or a wrong design decision when discovered otherwise. We cannot also forget about the most tricky coupling – a hidden coupling. We have to constantly learn how to design our dependencies right, in order to maintain coupling on the desirable level. After all, it seems to be one of the most influential forces which stand behind the quality of the software we produce.