Let’s assume that we have three components: A, B and C.
Another component, Main, has a dependency to A and wants to call a method foo that belongs to component C:
The previous snippet is equivalent to:
B b = a.getB();
C c = b.getC();
The problem with this code is that in order to call foo, Main needs to go all the way through components A and B to reach C and finally invoke that method. Main needs to know all the 3 components, along with their internal structure, in order to be able to invoke that call.
Although our example is oversimplified, we could say that Main needs to know which method to call in order to obtain B from A, C from B and call foo. Moreover, the intermediate components (B and C) need to provide some API to support these chained calls. Clearly, such API changes their nature. Component A should not act as a provider of component B but instead have an API that justifies its existence. The problem is that we described here is a violation of the Law of Demeter.
The Law of Demeter is a very simple guideline that helps us to write components that are not tightly coupled. The Wiki Page summarizes the Law of Demeter it the following 3 simple sentences:
- each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
- each unit should only talk to its friends; don’t talk to strangers.
- only talk to your immediate friends.
In OOP we learn that objects communicate with each other by passing messages, so a method call is actually message sent from the caller to the callee. For example, a.getB() simply means that Main sends a message to A“asking” for B. In the second snippet, it is obvious that Main is responsible for sending all the messages, leaving the intermediate components without any actual logic other than giving their internal components.
Now let’s think about what Main needs to do. It needs to invoke foo but all it knows is component A. It doesn’t need to “search” for all the other components. It doesn’t even need to know they exist. All it needs to do is to “tell” component A to find and call the method foo. A will probably delegate this task to B and B will do something similar with C. In this case, component A “tells” B what to do.
The Law of Demeter has some very beneficial implications. Each component interacts only with its direct dependencies. No indirect dependencies are known, which makes it easier to change their implementation independently. Imagine if we changed the implementation of component A to call some component X instead of component B. With chain calls, such decisions are propagated to the caller. Main has to change in order to adopt this change.
Testing is also, a lot easier. In our example, we would have to mock all the intermediate components, which is clearly a design smell. Actually, when we write tests, it is easy to identify the code smell by noticing how many useless mocks we have to create.
These are side effects of internal implementation leaking and poor encapsulation that lead to tightly coupled components. The Law of Demeter helps the design of the system by making the components more autonomous/decoupled, which is, in my opinion, the most important advantage.
Law of Demeter in microservices
In the world of microservices, components can be services located in different places across the network. The most important difference is that all the calls between components are now remote calls, thus failure is actually a possible scenario.
Consider in our example, if each getComponent() invocation was a remote call. How fragile would Main be when it does 3 remote calls to different components/services? Of course, even if we apply the Law of Demeter, 3 messages would have to be passed along the components in order to get the result of foo() but now Main communicates only with the 1 component that needs to. As we will see in following paragraphs, this change in the communication protocol has a very big gain in terms of availability and performance.
Usually, in order to tackle the problems of component failures and increase the performance and the resilience of our system we use event-driven architectures. In an event-driven architecture, the communication between microservices is achieved by sending an event to an event dispatcher instead of sending the message directly to the component we want to invoke. The callee listens for these events and processes them when they become available. These events are sent asynchronously to the dispatcher. More on event-driven architecture can be found in this post.
The invocation a.bar() is conceptually the same as sending the event with this invocation to a dispatcher where component A has subscribed to these events. This extra level of indirection decouples the components more since the caller doesn’t need to know how to call A but the fact that the communication is asynchronous means that in some cases the caller doesn’t have to wait for A to finish its processing. In our example, if foo() were actually a method that sends a notification or an email, the caller wouldn’t have to wait for this task to be completed.
Even if we use asynchronous communication in our example, when violating the Law of Demeter, there are still a few problems with the design due to the fact that the events that Main sends depend on each other. Main asks component A for B, in order to be able to ask component B for C etc. This chattiness is required for the construction of the final event. Main can’t send the event of invoking foo to C before B returns C (actually B will return the location of C). Also, in case one of the intermediate components/services is not available Main would not be able to construct the final event to send to C, thus Main would not be able to complete its process and, depending on the business rules, it could fail to accomplish its task. In this case, the unavailability of the indirect components is propagated to Main.
This design also has some problems in terms of performance. Main could have written the event of calling foo immediately and continue doing something useful instead of discovering other components.
All these problems are the implications of the fact that Main instead of “telling” what it needs it tries to find a way of completing a large part of its task by “asking” the other components to gather information. One could say that Main acts like it does not trust the other components!
Obviously, if we design our system in a way that doesn’t violate the Law of Demeter these problems go away almost for free. Main will send just 1 event and continue its tasks even if the other components are not available. Since Main sends only 1 event, there are no extra steps in order to create this event as there were previously so the communication protocol has been optimized. Clearly, the availability of Main doesn’t depend on the availability of the other components (except the event dispatcher). Main has completed its task just by “telling” what needs to be done. The other components will eventually get the event from the dispatcher when they become available again.
We should design our components to be as loosely coupled as possible and Law of Demeter is a good way to go towards that direction.