Why are circular links considered harmful?

Why is this a bad design for an object to refer to another object that refers to the first?

+71
design oop
Dec 13 '09 at 19:54
source share
11 answers

Circular dependencies between classes are not necessarily harmful. Indeed, in some cases they are desirable. For example, if your expression was about pets and their owners, you would expect the Pet class to have a method to get the pet owner, and the owner class to have a method that returns a list of pets. Of course, this can make memory management more difficult (in a language other than GC). But if circularity is inherent in the problem, then trying to get rid of it is likely to lead to more problems.

On the other hand, circular dependencies between modules are harmful. As a rule, this is an indicator of a poorly thought out module structure and / or rejection of the initial modulation. In general, a code base with uncontrolled cross-dependencies will be harder to understand and harder to maintain than one with a clean, layered module structure. Without decent modules, it can be much harder to predict the impact of changes. And this makes maintenance difficult and leads to "code corruption" as a result of a poorly thought out fix.

(In addition, build tools such as Maven will not process modules (artifacts) with circular dependencies.)

+70
Dec 14 '09 at 6:58
source share

Circular links are not always harmful - there are some use cases where they can be quite useful. Bilocal lists, graph models, and computer grammar models come to mind. However, as a rule, there are several reasons why you can avoid circular references between objects.

  • Alignment of data and graphs. Updating objects using circular references can create problems in ensuring that the relationships between objects are valid at all times. This type of problem often arises in implementations of object-relational modeling, where one can often find bi-directional circular references between objects.

  • Ensuring atomic operations. Ensuring that changes in both objects in a circular link are atomic can be difficult - especially with multithreading. To ensure consistency of a graphic that is accessible from multiple threads, special synchronization structures and locking operations are required to ensure that no thread sees an incomplete set of changes.

  • Problems of physical separation. If two different classes A and B access each other in a circular manner, it may be difficult to separate these classes from independent assemblies. Of course, it is possible to create a third assembly with the IA and IB interfaces that implement A and B; allowing everyone to refer to others through these interfaces. It is also possible to use weakly typed links (for example, an object) as a way to break the circular dependency, but access to the method and properties of such an object cannot be easily accessible, which can lead to the failure of the link target.

  • Providing immutable circular references. Languages ​​such as C # and VB provide keywords to allow links within an object to be immutable (read-only). Immutable links allow the program to ensure that the link refers to the same object for the lifetime of the object. Unfortunately, it is not easy to use the compiler forced enforcement mechanism to ensure that circular references cannot be changed. This can only be done if one object creates an instance of another (see Example below).

    class A { private readonly B m_B; public A( B other ) { m_B = other; } } class B { private readonly A m_A; public A() { m_A = new A( this ); } } 
  • Availability and ease of maintenance. Circular links are inherently fragile and easily broken. This is partly because reading and understanding code that includes circular references is more complicated than code that avoids them. Ensuring that your code is easy to understand and maintain helps to avoid errors and allows you to make changes more easily and safely. Circular reference objects are more complex than unit test, because they cannot be tested in isolation from each other.

  • Lifecycle management of objects. Although the .NET garbage collector is able to identify and process circular references (and correctly dispose of such objects), not all languages ​​/ environments can. In environments that use reference counting for their garbage collection scheme (for example, VB6, Objective-C, some C ++ libraries), circular references can lead to memory leaks. Since each object rests on a different one, their reference counters will never reach zero and, therefore, will never become candidates for collection and cleaning.

+58
Dec 22 '09 at 19:38
source share

Because now they are really one single object. You cannot test any of them.

If you change it, you will probably also come across your companion.

+8
Dec 13 '09 at 19:57
source share

From Wikipedia:

Cyclic dependencies can cause many unwanted effects in programs. Most problematic programs have a design perspective on interlocking interdependent modules that reduce or make it impossible to separately reuse a single module.

Circular dependencies can have a domino effect when a small local change in one module extends to other modules and has unwanted global effects (software errors, compilation errors). Circular dependencies can also lead to infinite recursions or other unexpected crashes.

Circular dependencies can also cause memory leaks by preventing certain very primitive automatic garbage collectors (those that use reference counting) from freeing up unused objects.

+7
Dec 13 '09 at 19:57
source share

Such an object can be difficult to create and destroy, because in order to do either non-atomically, you must violate referential integrity in order to first create / destroy one and the other (for example, your SQL database may stop this). This may confuse your garbage collector. Perl 5, which uses simple reference counting to collect garbage, cannot (without help) allow its memory leak. If two objects have different classes, now they are tightly connected and cannot be separated. If you have a package manager to install these classes, circular dependency extends to it. He needs to know to install both packages before testing them, which (acting as the developer of the build system) is PITA.

However, all this can be overcome and it is often necessary to have round data. The real world does not consist of neat directed graphs. Many graphs, trees, hell, double list are circular.

+5
Dec 13 '09 at 20:02
source share

This affects the readability of the code. And from circular dependencies on spaghetti code, there is only a tiny step.

+2
Dec 13 '09 at 19:59
source share

Here are a few examples that can help illustrate why circular dependencies are bad.

Problem No. 1: What is initialized / built first?

Consider the following example:

 class A { public A() { myB.DoSomething(); } private B myB = new B(); } class B { public B() { myA.DoSomething(); } private A myA = new A(); } 

Which constructor is called first? There really is no way to be sure, because it is completely ambiguous. One or the other DoSomething method will be called on an object that is not initialized., Which leads to incorrect behavior and, most likely, an exception. There are ways around this problem, but they are all ugly, and they all require non-constructor initializers.

Problem number 2:

In this case, I changed to uncontrolled C ++ - an example, because the .NET design implementation hides the problem from you. However, in the following example, the problem becomes quite clear. I am well aware that .NET does not use reference counting under the hood to manage memory. I use it here only to illustrate the main problem. We also note that I have demonstrated here one possible solution to problem No. 1.

 class B; class A { public: A() : Refs( 1 ) { myB = new B(this); }; ~A() { myB->Release(); } int AddRef() { return ++Refs; } int Release() { --Refs; if( Refs == 0 ) delete(this); return Refs; } B *myB; int Refs; }; class B { public: B( A *a ) : Refs( 1 ) { myA = a; a->AddRef(); } ~B() { myB->Release(); } int AddRef() { return ++Refs; } int Release() { --Refs; if( Refs == 0 ) delete(this); return Refs; } A *myA; int Refs; }; // Somewhere else in the code... ... A *localA = new A(); ... localA->Release(); // OK, we're done with it ... 

At first glance, you might think that this code is correct. The reference counting code is pretty straightforward. However, this code causes a memory leak. When A is built, it first has a reference count of "1". However, the encapsulated variable myB increments the reference counter, assigning it a counter of "2". When localA is freed, the counter decreases, but returns only to "1". Consequently, the object remains hanging and is never deleted.

As I mentioned above, .NET does not really use reference counting for its garbage collection. But it uses similar methods to determine if an object is still being used, or if it’s OK, to remove it, and almost all such methods may become confused with circular references. The .NET garbage collector claims to be able to handle this, but I'm not sure I trust it, because it is a very complex problem. On the other hand, the problem is related to the problem, simply not allowing circular links at all. Ten years ago, I would have preferred the .NET approach for its flexibility. These days, I prefer the Go approach for its simplicity.

+2
Dec 21 '09 at 16:39
source share

It is completely normal to have objects with circular links, for example. in a bidirectional domain model. An ORM with a correctly written data access component can handle this.

+1
Dec 22 '09 at 20:54
source share

Refer to the Lakoss book, in C ++ software development, cyclic physical dependency is undesirable. There are several reasons:

  • This makes it difficult to test them and it is impossible to reuse it yourself.
  • This makes it difficult for people to understand and maintain.
  • This will increase the cost of connection time.
+1
Nov 17 '13 at 12:00
source share

Circular links appear to be a legitimate domain modeling scenario. An example is Hibernate, and many other ORM tools support this cross-entity relationship to provide bi-directional navigation. A typical example in the online auction system, the seller can save a link to the List of persons that he / she sells. And each item can maintain a link to it by the respective seller.

+1
Jun 12 '14 at 4:26
source share

The .NET garbage collector can handle circular references, so there is no fear of memory leaks for applications running on the .NET platform.

-2
Dec 14 '09 at 6:35
source share



All Articles