(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?
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!
Avoiding circular dependencies by refactoring Circular dependencies create tight couplings between the classes or modules involved, which means both classes or modules have to be recompiled every time either of them is changed.
To remove the cycle using DI, we remove one direction of the two dependencies and harden the other one. To do that we should use an abstraction: in the order module, we add the DiscountCalculator interface. Thus, the Order class depends on this new type and the order module no longer depends on the customer module.
If you are predominantly using constructor injections then it is possible to create circular dependencies in Spring.
A circular dependency occurs when two classes depend on each other. For example, class A needs class B, and class B also needs class A. Circular dependencies can arise in Nest between modules and between providers. While circular dependencies should be avoided where possible, you can't always do so.
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 );
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With