Seriously, what's the big deal about having a few cycles in one's import graph? I see it as a lesser evil than having super fine grained imports that make keeping track of where to look for what code an absolute nightmare.
4 Answers
Depending on your environment, cyclic dependencies across modules can make static linking impossible. Module A cannot be linked until Module B is linked; but Module B depends on Module A and cannot be linked until Module A is linked. This is why some environments - such as .NET and Delphi - don't allow this sort of thing at all.
Other environments/compilers/linkers may be more forgiving, but that does not make it better design. While it seems to be generally accepted that cyclic dependencies within data structures are sometimes necessary (entity models, doubly-linked lists, certain kinds of trees... you get the picture), it is almost always a serious design flaw to have a cyclic module dependency.
Why? Try to picture the initialization phase of an application, while modules are still being loaded. Most modules count on the fact that all of their dependencies have been fully-loaded before running any initialization code. Cyclic dependencies invalidate this assumption, because it's physically impossible (at least in this universe) for both modules to be loaded before each other. Similarly, in the teardown phase, finalization code that makes perfectly reasonable assumptions may crash because a dependent module has already been unloaded by the runtime.
The worst part is that on most operating systems, when you have cyclic module dependencies, the load order is deterministic but impossible to predict. So an assumption about initialization order that turns out to be correct today may suddenly break tomorrow due a totally innocent change in a totally different part of the application. Tracking down these bugs can be an excruciatingly painful process.
Cyclic imports within a single package/module/assembly are really another story entirely. An "import" can mean so many different things depending on the context. In many cases they are "Considered Harmful" simply because early compilers couldn't handle them, so you had to do your own cycle detection with a bunch of #ifdef
directives (or similar) and obviously, after a certain number of these, you start to lose your mind. But in modern-day object-oriented programming it's often considered good practice to have one file per class, which means that a cyclic import is merely a sign of a cyclic class dependency, which as I mentioned above, isn't necessarily such a bad thing unless it's unnecessary.
If it's unnecessary, then it's a Bad Thing simply on account of the fact that any unnecessary complexity is a Bad Thing, and cyclic dependencies add complexity.
My big feeling on cyclical module dependencies is that if those two modules really do depend on one another, perhaps they should be one module. You can't use one without the other, so what's the point of splitting them up?
-
Well, you may need a small part of each one in the other. You could split these small parts into their own modules, but it would make imports annoyingly fine-grained.– dsimchaOct 14, 2010 at 18:33
-
4If one needs "a small part" of the other, that small part should be split out because it's obviously not the one class's core responsibility. I find general and long term code comprehensibility to be more important than typing an import line. Oct 14, 2010 at 19:24
-
You're apparently thinking in Java, where a class == a file/module (roughly speaking). I'm thinking in other languages, where you can put any amount of classes and free functions into a single file. Of course, a class needs to have a single responsibility, but a module/file IMHO can have multiple but related responsibilities.– dsimchaOct 15, 2010 at 16:20
-
3Substitute "module" for "class" then. A module should also have a single responsibility. Cyclic dependencies add complexity more quickly than non-cyclic dependencies. Oct 15, 2010 at 16:26
One reason is that it can be, but isn't always, a warning sign of a bad design decision somewhere.
Keeping them separate does force you to think about the system dependencies when adding new code. This is a good thing. When you have to think about the whole architecture, you are less likely to make a decision that tangles it up.