4

I'm designing an object dependency graph of my program and one ambiguity between design variants appears from time to time.

Imagine two objects having a reference to each other. Obviously, at least one reference should be assigned after object's initialization (for example, through a subscription method) and another one optionally can be a direct dependency.

Here's an object diagram

enter image description here

I'm not sure I've used correct UML, so here's my description:

1) Both objects have subscription/binding methods. Usage:

A a = new A();    
B b = new B();    
a.Set(b);    
b.Set(a);

2) ObjectB is a component of ObjectA:

A a = new A(new B());     

// somewhere, possibly in a's method :    
b.Set(a);

3) Third is actually an opposite of the second, no need to explain.

In my situation I can use any of these and my program will work, but I want some theoretical reasons. Can they be found?

astef
  • 328
  • 1
  • 10

2 Answers2

3

Disclaimer - I don't want to discuss if a cyclic reference is good or bad - IMHO there are situations where this kind of design is 100% appropriate, and for the sake of not starting a holy war, let's assume this is such a case here.

Your 3 alternatives don't differ much - at least, not in languages like C# or Java. What you get is two objects a and b, each holding a reference to each other. This is also true for (2) - if you see B as a component of A is more a question of your point of view (since classes are always reference types in these languages).

So the only difference between (2) and (1) is if you pass one of the two references within the constructor, or by using a separate Set method. Choose the "constructor" alternative if you want your cyclic references to be initialized immediately, after object construction, making sure they are as soon initialized as possible. Use the Set alternative if you need the option to delay the initialization (maybe you want to use single A objects without the need to provide always a B object - and vice versa - in some scenarios).

Final remark: it is much easier to make such a decision in terms of the context, knowing the names of the classes, the semantics, the context and the usage scenario.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • First of all, we are talking about objects and not classes. Cyclic references between objects are inevitable. Actually, there's no program without them. – astef May 23 '14 at 10:09
  • @astef: hold your breath, I agree with you - maybe you did not understand my disclaimer correctly? – Doc Brown May 23 '14 at 10:14
  • OK, I'll continue this topic at @BЈовић 's answer if needed ;) Direct component graph can have no cycles - that is the first significant difference. Run-time binding complicates the composition root - that is the second – astef May 23 '14 at 10:17
  • Yes, it is easier to make such a decision in the context, but I'm searching for theoretical answers, not practical. The problem is that in practice each of these variants can be transformed to another one with some general consequances in the application logic. I think I'll come up with own answer and you'll see what am I talking about – astef May 23 '14 at 11:55
2

As always, asking a question advances my own understanding of the issue.

The main difference between those three variants is object's lifetime. That what is absent when we are talking about classes, but it is very important in case of objects.

In first variant, lifetimes are independent. Both objects can live without each other. a's knowledge of b starts from receiving a message about it, the same as b's knowledge of a. The responsibility for connecting those object's lifetimes falls to a third object, so this variant must be used only when there is a real need to have a and b living without each other. That is rare, actually. I'm still searching for a good and simple example.

In the second variant a knows about b during all life. We say a owns b and use something like C#'s readonly modifier for a field with reference to b. But b can't own a, because it will be impossible to instantiate them. In other words, direct component graphs can't have cycles. Instead, we can use a's right to configure it's own components, to inroduce itself to b. In this case we don't need third object to connect these two, but we must complicate a's state with additional field like isInitialized which must be checked each time we use a.

astef
  • 328
  • 1
  • 10
  • I think its debatable if the lifetime of the objects is the cause for the for of initialization, or if it is the result. Most probably its neither - the real cause for choosing the one or the other form of implementation lies in the requirements or the environment. And the example you are looking for (1) is simply if you want to be able to unit test A and B completely independent from each other (which nowadays should be a frequent case). – Doc Brown May 23 '14 at 13:41
  • @DocBrown Again you're mixing classes with objects. In all examples I am able to test `A` and `B` independently. They even can be defined in separate assemblies without dependecy between this assemblies. – astef May 23 '14 at 13:48
  • Your reasoning about lifetime and requirements is not clear. Objects' lifetime IS the requirement for any object. – astef May 23 '14 at 13:56