Why support cyclic dependencies at all? Are there valid use cases?

Background

Time ago I learnt something about package design, in particular about loose coupling:

The Acyclic Dependencies Principle

The dependency structure between packages must be a Directed Acyclic Graph (DAG). That is, there must be no cycles in the dependency structure.

Robert Cecil Martin aka uncle Bob, Granularity

The questions

  1. Why are there systems like nodejs (where I work the most, but question may apply to others) supporting cyclic dependencies between modules? To clarify: Within nodejs, a module can be either one source code file or one folder containing a package.

  2. Are there any real-life use cases considered to be valid? Please, not being “examples of bad design” (explained below)

Examples of bad design

Question (2) refers to examples where there are cyclic dependencies which would be better refactored to avoid them, resulting on a better design.

As an example of what I mean with the preceding assertion, see this answer by Doc Brown where he exposes the “file system example”, but please note this question is not about cyclic dependencies between Classes.


Related Questions at programmers SE

  • Why are cyclic imports considered so evil?
  • Is there a valid case for two components to depend on each other?

7

Cyclic module dependencies frequently crop up when you have a module that serves as an interface between user code and (several) implementation modules.

Typically, such an interface module defines types and generic functionality for the implementation modules to use, which is why the implementation modules depend on the interface module. However, to provide a true abstraction to the user code (= avoid user code dependencies on the implementation modules), the interface module has to interact with the different implementation modules, and thus depend on them.

If you replace “module” with “class”, this becomes a lot clearer: The interface module is an abstract class that provides the entire user interface. The different implementations inherit from the abstract class and thus depend on the abstract interface. However, to make the very existence of the subclasses invisible to the user, the abstract class also has to provide factory methods that select the different implementations under the hood. Since the factories need to instanciate the concrete classes, they depend on them.

Of course, such a class architecture would usually be encapsulated within a single module, so you don’t get circular module dependencies yet. However, the same structure can apply to code in a larger context, leading to a situation where you would have true cyclic module dependencies.

Usually, such cyclic dependencies can be broken up by splitting the abstract interface into two parts, one on which the implementation modules depend, and one that depends on the implementation modules. This usually has the downsides of 1. one module being too small to justify it being a module, and 2. adding complexity to the public interface since user code now has to depend on two modules directly.

Another approach to break the cyclic dependencies would be for the implementation modules to register themselves with the interface module. This too has two downsides: 1. It forces a single callback interface into the implementation modules, which may not be appropriate, and 2. it significantly adds complexity for the registering and the handling of the registered implementations within the interface module.

So, yes, there are valid situations where restriction to non-circular module dependencies leads to more complex code, making the use of a dependency cycle the preferable alternative. And good programming languages allow for this.

1

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *