How to Break a Cyclic Dependency between ES6 modules
Based on Typescript examples
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
Introduction
One of the rare books fully dedicated to modularity patterns is Java Application Architecture: Modularity Patterns with Examples Using OSGi written by Kirk Knoernschild. Among the important sections in this book is the chapter Acyclic Relationships which explains three techniques to break cyclic dependencies to make the dependency structure a Directed Acyclic Graph (DAG) as required by the Acyclic Dependencies Principle (ADP).
In this article, we apply these three techniques to ES6 modules. So, let’s start by introducing the example on which we will carry out the techniques.
Cyclic Dependency Example
In this section, we present the starting code that will be refactored in the following sections. There are two cycle types between modules: direct and indirect. Our example shows just the first type. The source code of the different versions is available here.
In our original codebase, we have two modules:
- An order module containing an Order class
- A customer module containing a Customer class
In the two modules, we have two root concepts:
- Customer
- Order
and we have two behaviors associated with them:
- calculate customer discount based on customer orders.
- calculate order discount based on customer discount.
The two modules customer.ts and order.ts depend on each other. So, the dependency graph is a cyclic one.
Let’s break this cycle.
Breaking Cyclic Dependency Techniques
The most important techniques to break dependency cycles are:
- Dependency Inversion (aka. Callback)
- Escalation
- Demotion
In this section, we cover the three techniques and also a more basic one: Merging. After that, we will try to draw a classification tree of all the strategies.
Dependency Inversion (aka Callback)
To remove the cycle using DI, we remove one direction of the two dependencies and harden the other one. To do that we should use an abstraction: in the order module, we add the DiscountCalculator interface. Thus, the Order class depends on this new type and the order module no longer depends on the customer module.
The customer class is still responsible for calculating the discount, but this time it implements this behavior as a DiscountCalculator class.
The dependency graph becomes now a DAG:
The dependency is now unidirectional and the cycle is removed.
Escalation
With the first technique, to remove the cycle we just edited the dependencies of a single module. In the next two techniques (escalation and demotion), we will delegate the discount calculation to a new module and we will introduce two more dependencies to/from the new module.
The customer calculates the discount based on its orders.
And the Order just stores the raw amount. It’s no longer responsible for calculating its discounted amount.
It’s now the role of the OrderDiscounter to calculate the discounted amount.
The new dependency graph becomes the following:
The dependency graph becomes also a DAG, but with a new module and two new arrows.
Demotion
As the escalation, to break the cycle demotion add a new module but with two ingoing dependencies. The new module contains the new class OrderDiscount:
The Order class uses the OrderDiscount to calculate its discountedAmount:
The customer is responsible for creating the OrderDiscount instance that will be passed to the Order.
The new dependency graph becomes the following:
Like the escalation graph, the demotion’s dependency graph becomes also a DAG with a new module and two new arrows. But, this time the direction of the arrow is inverted.
Merge modules
The other way to break the dependency cycle in our example is to merge the two modules in a single one. This is a simple strategy but it is not the best one when the cohesion between the two modules is low.
We should also note that by merging our two modules we remove the cycle at the modules level but it stays present at the classes level. If we want to break the cycle at the classes level we can use the same techniques.
Taxonomy
After showing the cyclic dependency breaking strategies, let’s draw a taxonomy diagram as a summary:
Expect the remove the two modules strategy, we already discussed the other strategies. So, we don’t repeat our self and we go straight to the conclusion.
Conclusion
In his book, Kirk Knoernschild carried the patterns from the C++ to the Java world. In this post, we’ve tried to do the same thing by promoting the terminology to the Javascript and Typescript world. The Java world is more mature and rich than the front-end ecosystem. So, it should be more and more inspired by the backend experience.
Before we finish this article, I would like to note two things:
- We presented each technique in the context of breaking a direct cycle but they can also be used to break indirect cycles.
- Dependency cycle isn’t absolute evil. For example, in some cases, the cycle is allowed if it is local to a layer. But, the cycle shouldn’t exist between release units.
Finally, you can find Kirk’s code that inspired this post on his GitHub here.