[ACCEPTED]-Why are circular references considered harmful?-oop
Circular dependencies between classes are not necessarily 25 harmful. Indeed, in some cases they are 24 desirable. For example, if your application 23 dealt with pets and their owners, you would 22 expect the Pet class to have a method to 21 get the pet's owner, and the Owner class 20 to have a method that returns the list of 19 pets. Sure, this can make memory management 18 more difficult (in a non-GC'ed language). But 17 if the circularity is inherent in the problem, then 16 trying to get rid of it is probably going 15 to lead to more problems.
On the other hand, circular 14 dependencies between modules are harmful. It is 13 generally indicative of a poorly thought-out 12 module structure, and/or failure to stick 11 to the original modularization. In general, a 10 code-base with uncontrolled cross-dependencies 9 will be harder to understand and harder 8 to maintain than one with a clean, layered 7 module structure. Without decent modules, it 6 can be much harder to predict the effects 5 of a change. And that makes maintenance 4 harder, and leads to "code decay" resulting 3 from ill-conceived patching.
(Also, build 2 tools like Maven won't handle modules (artefacts) with 1 circular dependencies.)
Circular references are not always harmful - there are some use cases where they can 65 be quite useful. Doubly-linked lists, graph 64 models, and computer language grammars come 63 to mind. However, as a general practice, there 62 are several reasons why you may want to 61 avoid circular references between objects.
Data and graph consistency. Updating 60 objects with circular references can create 59 challenges in ensuring that at all points 58 in time the relationships between objects 57 are valid. This type of problem often arises 56 in object-relational modeling implementations, where 55 it's not uncommon to find bidirectional, circular 54 references between entities.
Ensuring atomic operations. Ensuring that 53 changes to both objects in a circular reference 52 are atomic can become complicated - particularly 51 when multithreading is involved. Ensuring 50 consistency of an object graph that is accessible 49 from multiple threads requires special synchronization 48 structures and locking operations to ensure 47 that no thread sees an incomplete set of 46 changes.
Physical separation challenges. If two different classes A and 45 B reference each other in a circular fashion, it 44 can become challenging to separate these 43 classes into independent assemblies. It's 42 certainly possible to create a third assembly 41 with interfaces IA and IB that A and B implement; allowing 40 each to reference the other through those 39 interfaces. It's also possible to use weakly 38 typed references (e.g. object) as a way 37 to break the circular dependency, but then 36 access to the method and properties of such 35 an object couldn't be easily accessed - which 34 can defeat the purpose of having a reference.
Enforcing immutable circular references. Languages 33 like C# and VB provide keywords to allow 32 references within an object to be immutable 31 (readonly). Immutable references allow a 30 program to ensure that a reference refers 29 to the same object for the lifetime of the 28 object. Unfortunately, it's not easy to 27 use the compiler-enforced immutability mechanism 26 to ensure that circular references cannot 25 be changes. It can only be done if one object 24 instantiates the other (see C# 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 ); } }
Program readibility and maintainability. Circular 23 references are inherently fragile and easy 22 to break. This stems partly from the fact 21 that reading and understanding code that 20 includes circular references is harder than 19 code that avoids them. Ensuring that your 18 code is easy to understand and maintain 17 helps to avoid bugs and allows changes to 16 be made more easily and safely. Objects 15 with circular references are more difficult 14 to unit test because they cannot be tested 13 in isolation from one another.
Object lifetime management. While .NET's 12 garbage collector is capable of identifying 11 and dealing with circular references (and 10 correctly disposing such objects), not all 9 languages/environments can. In environments 8 that use reference counting for their garbage 7 collection scheme (e.g. VB6, Objective-C, some 6 C++ libraries) it is possible for circular 5 references to result in memory leaks. Since 4 each object holds on to the other, their 3 reference counts will never reach zero, and 2 hence will never become candidates for collection 1 and cleanup.
Because now they're really one single object. You 3 can't test either one in isolation.
If you 2 modify one, it's likely that you affect 1 its companion as well.
From Wikipedia:
Circular dependencies can 16 cause many unwanted effects in software 15 programs. Most problematic from a software design 14 point of view is the tight coupling of 13 the mutually dependent modules which reduces 12 or makes impossible the separate re-use 11 of a single module.
Circular dependencies 10 can cause a domino effect when a small 9 local change in one module spreads into other 8 modules and has unwanted global effects 7 (program errors, compile errors). Circular 6 dependencies can also result in infinite 5 recursions or other unexpected failures.
Circular 4 dependencies may also cause memory leaks 3 by preventing certain very primitive automatic 2 garbage collectors (those that use reference counting) from 1 deallocating unused objects.
Such an object can be difficult to be created 20 and destroyed, because in order to do either 19 non-atomicly you have to violate referential 18 integrity to first create/destroy one, then 17 the other (for example, your SQL database 16 might balk at this). It might confuse your 15 garbage collector. Perl 5, which uses simple 14 reference counting for garbage collection, cannot 13 (without help) so its a memory leak. If 12 the two objects are of different classes 11 now they are tightly coupled and cannot 10 be separated. If you have a package manager 9 to install those classes the circular dependency 8 spreads to it. It must know to install 7 both packages before testing them, which (speaking 6 as a maintainer of a build system) is a 5 PITA.
That said, these can all be overcome 4 and its often necessary to have circular 3 data. The real world is not made up of 2 neat directed graphs. Many graphs, trees, hell, a 1 double-linked list is circular.
It hurts code readability. And from circular 2 dependencies to spaghetti code there is 1 just a tiny step.
Here are a couple of examples that may help 47 illustrate why circular dependencies are 46 bad.
Problem #1: What gets initialized/constructed 45 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 44 constructor is called first? There's really 43 no way to be sure because it's completely 42 ambiguous. One or the other of the DoSomething 41 methods is going to be called on an object 40 that's uninitialized., resulting in incorrect 39 behavior and very likely an exception being 38 raised. There are ways around this problem, but 37 they're all ugly and they all require non-constructor 36 initializers.
Problem #2:
In this case, I've 35 changed to a non-managed C++ example because 34 the implementation of .NET, by design, hides 33 the problem away from you. However, in the 32 following example the problem will become 31 pretty clear. I'm well aware that .NET doesn't 30 really use reference counting under the 29 hood for memory management. I'm using it 28 here solely to illustrate the core problem. Note 27 also that I've demonstrated here one possible 26 solution to problem #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, one 25 might think that this code is correct. The 24 reference counting code is pretty simple 23 and straightfoward. However, this code results 22 in a memory leak. When A is constructed, it 21 initially has a reference count of "1". However, the 20 encapsulated myB variable increments the 19 reference count, giving it a count of "2". When 18 localA is released, the count is decremented, but 17 only back to "1". Hence, the object is left 16 hanging and never deleted.
As I mentioned 15 above, .NET doesn't really use reference 14 counting for its garbage collection. But 13 it does use similar methods to determine 12 if an object is still being used or if it's 11 OK to delete it, and almost all such methods 10 can get confused by circular references. The 9 .NET garbage collector claims to be able 8 to handle this, but I'm not sure I trust 7 it because this is a very thorny problem. Go, on 6 the other hand, gets around the problem 5 by simply not allowing circular references 4 at all. Ten years ago I would have preferred 3 the .NET approach for its flexibility. These 2 days, I find myself preferring the Go approach 1 for its simplicity.
It is completely normal to have objects 4 with circular references e.g. in a domain 3 model with bidirectional associations. An 2 ORM with a properly written data access 1 component can handle that.
Refer to Lakos’s book, in C++ software design, cyclic 2 physical dependency is undesirable. There 1 are several reasons:
- It makes them hard to test and impossible to reuse independently.
- It makes them difficult for people to understand and maintain.
- It will increase the link-time cost.
Circular references seem to be a legitimate 9 domain modelling scenario. An example is 8 Hibernate and many other ORM tools encourage 7 this cross association between entities 6 to enable bi-directional navigation. Typical 5 example in an online Auction system, a Seller 4 entitiy may maintain a reference to the 3 List of entities He/She is selling. And 2 every Item can maintain a reference to it's 1 corresponding seller.
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.