8

(I tagged this as both C# and Java, since it's the same question in both languages.)

Say I have these classes

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private /*readonly*/ FileManager fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new FileManager(this); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}

The problem with this design is that as soon as FileManager tries to do anything inside its constructor with kernel (which it might reasonably need to), it will be calling a virtual method on a potential subclass instance whose constructor is not yet called.

This problem doesn't occur in languages where you can define true constructors (rather than initializers, like C#/Java), since then the subclasses don't even exist before their constructors are called... but here, this problem happens.

So what is the best/proper design/practice, to ensure this doesn't happen?

Edit:

I'm not necessarily saying I need circular references, but the fact is that both Kernel and FileManager depend on each other. If you have a suggestion on how to alleviate this problem without using circular references, then that's great too!

Community
  • 1
  • 1
user541686
  • 205,094
  • 128
  • 528
  • 886
  • Can you clarify this statement? "This problem doesn't occur in languages where you can define true constructors (rather than initializers, like C#)... but here, it does." – asawyer May 18 '12 at 20:24
  • @asawyer: Yup, see my update. (I'm referring to Python/C++, although they are both slightly different from each other in this regard.) In C++, derived members don't exist until the subclass's constructor gets called; in Python, there are "class methods" called constructors, which return a new member of a class (rather than merely initializing the new instance). Both of them sorta 'go around' this problem, although they have their own gotchas. – user541686 May 18 '12 at 20:24
  • 1
    I have to admit I've never faced this problem. All the times I had something similar, the second object (the 'subcomponent') never did anything with the main object in its constructor, other than saving it for future use, which poses no problem. I guess if I did encounter it, I would put the 'problematic' code (where the subcomponent calls the component back) outside the subcomponent's constructor, and call it only when the main component is ready. – zmbq May 18 '12 at 20:43

4 Answers4

5

To me, having circular dependencies between this kind of objects smells badly.

I think you should decide which object is the main one, and which one is the subject for aggregation, or even composition. Then construct the secondary object inside of the main one, or alternatively, inject it as a dependency of the main object. Then let the main object register its callback methods in the secondary object, which will call them whenever it needs to communicate with the "outer world".

If you decide that the relation type is aggregation, then once the main object is to be destroyed, it will unregister all the callbacks.

And if you go with composition, then just destroy the secondary object when the main one is being destroyed.

Here's an example of what I mean:

class Program
{
    static void Main( )
    {
        FileManager fm = new FileManager( );
        Kernel k = new Kernel( fm );
        fm.DoSomething( 10 );
    }
}

class Kernel
{
    private readonly FileManager fileManager;
    public Kernel( FileManager fileManager )
    {
        this.fileManager = fileManager;
        this.fileManager.OnDoSomething += OnFileManagerDidSomething;
    }

    ~Kernel()
    {
        this.fileManager.OnDoSomething -= OnFileManagerDidSomething;
    }

    protected virtual void OnFileManagerDidSomething( int i )
    {
        Console.WriteLine( i );
    }
}

class FileManager
{
    public event Action<int> OnDoSomething;

    public void DoSomething( int i )
    {
        // ...

        OnDoSomething.Invoke( i );
    }
}
Dmytro Shevchenko
  • 33,431
  • 6
  • 51
  • 67
  • Would you mind giving an example of what you mean (i.e. code)? (Btw, here, `Kernel` is definitely the "main" one, and `FileManager` is a subcomponent.) – user541686 May 18 '12 at 20:34
  • Ah, thanks. Hmm.. if you're using events, then how do you make sure something doesn't get called after `OnDoSomething += OnFileManagerDidSomething;` executes, but before the constructor ends? (Note that `FileManager` might do something when you say `+=`, perhaps to get some hash key or whatever from your kernel.) I guess the ultimate answer might just be "be careful" :) so if that's the answer then that works too... – user541686 May 18 '12 at 20:50
  • @Mehrdad Well, the main thing is that FileManager doesn't know anything about the Kernel. So Kernel doesn't really care what FileManager does, since it can only be affected by FileManager via the event handlers the Kernel totally controls. – Dmytro Shevchenko May 18 '12 at 20:52
  • Right, but how do you know the event handlers aren't overridden? – user541686 May 18 '12 at 20:52
  • @Mehrdad Events are multicast in C#. So there may be multiple event handlers registered for one event. You can go further and only allow adding new handlers, but not deregistering existing ones... or add more advanced logic to that. All via `add {...} remove {...}` syntax. – Dmytro Shevchenko May 18 '12 at 20:55
  • Nonono that's not what I mean. What I'm saying is, `OnFileManagerDidSomething` is virtual, so doesn't that cause the same problem we were trying to solve? – user541686 May 18 '12 at 20:56
  • @Mehrdad Then, the simplest solution is to use injection via property, instead of injection via constructor. Then you know for sure that the object is fully constructed. – Dmytro Shevchenko May 18 '12 at 21:00
  • By the way, aren't there only two possible ways to screw things up? 1) FileManager calls the event immediately when it's being added (bad, shouldn't happen); 2) FileManager is accessed from a separate thread right when an event is being added (there's a lot to think about, I guess). – Dmytro Shevchenko May 18 '12 at 21:03
  • No, there's a 3rd way: FileManager calls the *kernel* for some information when adding it as an event. (Don't forget that C# events are **not** necessarily simple adders/removers on top of delegates. That's only in the default implementation. Windows Forms, for example, use a hashtable -- and any mechanism of that sort would require a hash value for the owner (kernel) to be calculated, which might be a virtual method call.) – user541686 May 18 '12 at 21:14
  • How can `FileManager` call `Kernel` for some information when it doesn't know anything about `Kernel`? It only sees the method that is added, and it doesn't know where it belongs to. Or do you mean that the CLR may try to access the `Kernel` object as a result of `FileManager`'s actions? You got me intrigued, can you elaborate please? – Dmytro Shevchenko May 18 '12 at 21:19
  • I was thinking what would happen if `FileManager`'s event adder used the `Delegate.Target` property to access the kernel, but I guess "correct" code wouldn't do that (unless it really knew what it was doing), so never mind, I think this works, thanks. :) +1 – user541686 May 18 '12 at 21:34
2

Personally, I don't like circular references. But if you decide to leave them, you may add some laziness:

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private readonly Lazy<FileManager> fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new Lazy<FileManager>(() => new FileManager(this)); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}  

Laziness here lets ensure, that IKernel implementation will be initialized completely, when FileManager instance will be queried.

Dennis
  • 37,026
  • 10
  • 82
  • 150
  • +1 seems like a good option. (Kinda feel silly I didn't see this solution...) – user541686 May 18 '12 at 20:45
  • This only works until `fileManager` is used in the constructor. So now, instead of having to be careful in `FileManager` constructor, you have to be careful in `Kernel` constructor. The problem just shifts its weak spot. Also note that `Kernel` subclasses are vulnerable just the same. – Dmytro Shevchenko May 19 '12 at 08:47
  • @Shedal: to be more precise... If "Kernel" constructor doesn't call fileManager.Value, this works fine. Also, this is **not** an universal cure. I think, Mehrdad understand this. – Dennis May 21 '12 at 05:44
2

If you need to keep pairs of objects, with references to each other, you should provide a utility to build them correctly. Use the Factory pattern, and reduce the complexity of construction by hiding the construction and assembly behind the Factory pattern methods.

In Java, put the constructor in the package, and make the internal component's constructors and initial assignment set up methods "package private"

public Kernel newKernel() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return kernel;
}

public Filesystem newFilesystem() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return filesystem;
}

A similar idea can be had in C++ with thoughtful use of private and friend.

Edwin Buck
  • 69,361
  • 7
  • 100
  • 138
0

Although there isn't any declarative support for enforcing such a construct, I would suggest that one defines (via comments) a category of leak-safe constructors and parameters subject to the following constraints:

  1. A leak-safe constructor may use leak-safe parameters only to call call nested leak-safe constructors (where the parameters must also be passed as leak-safe), or store them in its own fields.
  2. A leak-safe constructor may not dereference any leak-safe parameters, nor any field that is loaded from a leak-safe parameter.
  3. A leak-safe constructor may not pass the object under construction anywhere except as a leak-safe parameter to a nested leak-safe constructor.
  4. An object constructed by calling passing a leak-safe parameter or the object under construction to a leak-safe constructor will be subject to all of the constraints of a leak-safe parameter.

If one abides by such constraints, it should be possible to have leak-safe constructors produce objects that mutually refer to each other, in a manner which can be statically demonstrated never to any partially-constructed objects be dereferenced outside their constructors (the constructor of Foo may pass the object under construction to the constructor of Bar, but if that constructor neither dereferences the passed-in object nor exposes it to any code that might do so, nor persists it anywhere outside itself, the only way it could get dereferenced would be by dereferencing the newly-created instance of Bar; if Foo's constructor doesn't do that, it won' get dereferenced until Foo's constructor returns).

supercat
  • 77,689
  • 9
  • 166
  • 211