My code base makes pretty heavy use of interfaces/abstractions, factories, repositories, dependency injection and other design patterns in an attempt to write good, maintainable code. My DI container is SimpleInjector. However, I've come across an indirect circular dependency problem that I'm struggling to see how to break without violating good design (and my own principles!).
The following semi-pseudo-code represents a simplification of the problem (note: in the interest of brevity, I don't detail the interfaces here, but they can be inferred from the classes that implement them, and I also don't show all the trivial stuff like the c'tor's setting backing fields - that's implied). Also note I use constructor injection throughout.
class A : IA
{
A(int id, IBRepository br);
IEnumerable<IB> GetBs(); // uses _br and _id to find all "child" B's.
int _id;
IBRepository _br;
}
class B : IB
{
B(int id, int aId, IARepository ar);
IA GetA(); // uses _ar and _aId to find "parent" A.
int _id;
int _aId;
IARepository _ar;
}
class ARepository : IARepository
{
ARepository(IAFactory af);
IA FindById(int id); // uses _af to create A if Id found.
IAFactory _af;
}
class BRepository : IBRepository
{
BRepository(IBFactory bf);
IB FindById(int id); // uses _bf to create B if Id found.
IBFactory _bf;
}
class AFactory : IAFactory
{
AFactory(IBRepository br);
IA Create(); // _br fed in to A c'tor
IBRepository _br;
}
class BFactory : IBFactory
{
BFactory(IARepository ar);
IB Create(); // _ar fed in to B c'tor
IARepository _ar;
}
And here's some sample usage code:
// Sample usage code
IARepository ar = Container.GetInstance<IARepository>();
IA a = ar.FindById(123);
IEnumerable<IB> bs = a.GetBs(); // get children.
IB b = bs.First();
IA a2 = b.GetA() // get parent.
Debug.Assert(a.Id == a2.Id);
The container gets wired up (not shown) by registering the concrete classes with each of their respective interfaces (using RegisterSingle<T>
for singleton/non-transient lifetime).
The problem is that I've introduced an indirect circular dependency as shown here (in reality the problem manifests itself as an exception as the application bootstraps - the SimpleInjector DI container does a nice job of telling you the problem!):
// Circular dependency chain
AFactory
BRepository
BFactory
ARepository
AFactory // <-- circular dependency!
As you can see, I'm already using Id's on A
and B
in an attempt to mitigate circular dependency issues that could arise from storing direct object references on each class (I've been bitten by that before).
I considered breaking the parent/child relationships (A.GetBs()
and B.GetA()
) out of the domain model's completely and pushing that functionality in to some kind of separate lookup service, but that seems like a code smell to me as the domain models should encapsulate their own relationships. Besides, the repositories already serve the purpose of such lookup functionality (which A
and B
already hold).
Most importantly though, this would make client code of A
and B
more complicated (consumers are used to being able to seamlessly "dot" their way through object graphs). Additionally, this would hurt performance as rather than caching a parent/child object reference on A
and B
, client code calling some separate service to do lookups would require constant trips to the backing data store (assuming no repository caching) and joins on the Id's.
As the saying goes, most software problems can be solved by another level of abstraction or indirection, and I'm sure that will be the case here, but I haven't been able to figure it out yet.
I've reviewed the following, and while I think I grasp what they're telling me, I'm struggling to relate it to my problem set:
- https://softwareengineering.stackexchange.com/questions/253646/how-to-handle-circular-dependency-in-dependency-injection
- http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/
- how to avoid circular dependencies here
So to summarize, how do I break the circular dependency problem while still using constructor DI, adhering to and utilizing established design patterns and best practices, and ideally maintaining a nice API for consumers of A
and B
to "dot" their way through the relationships? Any assistance would be greatly appreciated.