Dependency cycles
We can't discuss coupling without discussing Dependency cycles. A dependency cycle (otherwise known as a Circular dependency) is when an item depends on something else and that something else, or its dependants, depend on that item. Where this presents a problem is at the deployment level. If something in assembly A depends on something in assembly B and something in assembly B depends on something in assembly A, the compiler won't be able to figure out which project to build first and generate an error. Assembly cycles are easy to detect, you've got an error to deal with. The Acyclic Dependency Principle details that the dependency structure of packages must have no cycles.
Package is a grouping of elements. How the elements are grouped is somewhat subjective, but generally means a physical grouping (for example, assembly in .NET). However, it can be interpreted as any grouping (for example, namespace).
I tend to view packages as all groupings: from assembly to class. The real problem with dependency cycles rears its head at the assembly level, but the assembly structure is dependent on the deployment requirements of the system. The deployment requirements of the system are independent of the logical design of the system. Sometimes, the physical deployment requirements of the system are not known when initial design is begun, and often changes throughout the evolution of a system.
Dependency cycles that don't span assembly boundaries aren't as easy to detect. If class A uses class B and class B uses class A and they're all in the same assembly, there will be no errors. That may or may not be a bad thing. Obviously, it hinders your ability to maintain the code such that you cannot break those two classes out into their own assembly.
Proper dependency design
Dependencies between packages should always occur in one direction. There are various guidelines about what direction makes for the most maintainable code. One guideline deals with stability. The Stable Dependencies Principle (DSP) details that a package should only depend on other packages that are more stable than it.
One means of providing stability is abstractiveness. The more abstract something is, the more likely it can be stable. Interfaces, for example, since they only contain a contract and no code, have no possibility that a code change will cause instability with an interface. This assumes that thorough analysis went into the design of the interface. This means the best kind of dependency is a dependency upon an abstraction. The corollary to that is that abstractions should never depend on concrete implementation.
Depending upon abstractions is covered in more detail in Chapter 7.
Another proper dependency design attribute has to deal with layering. Layers can be physical and explicit or logical and implicit. A layer is some sort of abstraction grouping of elements with a common goal. Some common contemporary layers are the Data Layer and the User Interface Layer. Layers have levels. Some are lower than other layers. The Data Layer, for example, is a lower-level layer compared to the User Interface Layer, which is a higher-level layer. Proper dependency structure between layers is always such that lower-level layers never depend on higher-level layers. The abstraction between layers means a lower-level layer can be used by any number of higher-level layers. Take the User Interface Layer and Data Layer layers for example. I may have multiple User Interface Layers: a WinForm layer, a WPF layer, a Web layer, and so on. If my Data Layer depended on one of the User Interface layers, that User Interface layer would have to be deployed with all other user interface layers. It would be catastrophic if we had to deploy WinForm code with our Web User Interface Layer on a web server, for example.
We'll get more into refactoring as it relates to layers in Chapter 8.