Circular dependency tree, justified or not
Asked Answered
R

1

4

I've came up with some solution on which my IoC/DI container (Castle Windsor) is claiming that there's a cyclic dependency tree. And it's true. But I'm not that sure that this cycle is that harmful.

This is, more or less, the dependency tree:

  • A WebAPI controller depends on...
  • ...a service A depends on...
  • ...an unit of work depends on...
  • ...a repository depends on...
  • ...a domain event manager(1) depends on many...
  • ...domain event handlers, and one depends on...
  • ...service A(2)

(1) A domain event manager is a generalized class which aims to coordinate concrete domain events to be listened by the same or other domains and perform side actions.

(2) Here's where the dependency cycle happens

My domain event management and handling are implemented with aspect-oriented programming in mind, hence, while it's part of the dependency tree, a given domain event handler may or may not depend on a service in the same dependency tree: I consider the domain event handler like an additional top-level dependency. But the worst case has already happened.

My point is that, since a domain event is a cross-cutting concept, a given handler should be able to inject any service, even some one that's already in the dependency tree of a given operation flow.

For now, I've fixed the issue using property injection in the affected domain event handler, but anyway there could be an alternative to the whole workaround.

Rhodie answered 18/4, 2017 at 12:26 Comment(0)
J
4

From your abstract definition of your design it is hard to be exact, but I experienced cyclic dependencies for most cases to be the result of Single Responsibility Principle violations.

Ask yourself this: can the problem be resolved by breaking service A into multiple smaller independent components, where:

  • A WebAPI controller depends on a new service Y that again depends on one or multiple components of the now split service A.
  • Where one domain event handler depends on one of the segregated components of the former service A.

If the answer to that question is: yes, it is very likely that service A was too big, and took too much responsibility.

These issues are often closely related to the Interface Segregation Principle, because you will end up with smaller components with a more focused API. More than often those components will have just one public method.

Chapter 6 of the book Dependency Injection in .NET, second edition, goes into much detail about Dependency Cycles and them being caused by SRP violations.

Jungle answered 18/4, 2017 at 13:0 Comment(11)
I see your point. It's a possible solution: the method I need to call from Service A on the whole handler could be defined in another interface which is also implemented by Service A. While it's a good solution from the design standpoint, I'm very concerned about what kind of architecture can produce this approach in the long term...Quickstep
"what kind of architecture can produce this approach in the long term". That would be a Clean Architecture or any architecture that is based on the SOLID principles.Jungle
I'm not religious... SOLID principles aren't that solid per se. And I'm not that sure that arbitrary segregation should be the long term solution. Let me continue the discussion in the next comment:Quickstep
Actually, the domain event handlers are injected as an IList<T> and passed dependencies are dependencies of repositories. In addition, the domain event handlers can have their own dependencies. I mean, those domain event handlers shouldn't be part of the dependency tree but just cross-cutting concerns, but due to that C# has no language support to AOP, I need to inject those domain event handlers into the repository.Quickstep
This is the actual discussion and the reason to ask if the circular dependency is justified or not. In other situations, your reasoning (SOLID, Clean Architecture...) is absolutely valid, but on this case I've pushed the limits of OOP and that's why I'm trying to solve the problem without arbitrary approaches. I find that creating interfaces just to avoid this exceptional circular dependency tree case (not all ones) might increase overall solution complexity.Quickstep
To me this is not about "creating interfaces just to avoid circular dependencies". My observation in the last years has been that if I design my systems according to SOLID, it leads to far more maintainable and flexible systems, and lot's of the problems we typically have on a day-to-day basis (such as the problem of cyclic dependencies) go away (almost) completely. I don't feel that there's any "limit of OOP" here, although I must radmit that SOLIDly designed systems start to 'feel' kind of functional (even though I write all my software in C#).Jungle
In your case however, the probem might go away as well, when you start sending the messages to a durable queue. You seem to currently seem to process the handlers either in the same unit of work / transaction, or you risk losing information because no durable queue is involved. When introducing a queue, you naturally break the dependency graph, since message handlers will be resolved in a completely new scope and will be a root type of a new graph. The cyclic dependency will be gone.\Jungle
Exactly, Steven! Some of the events are handled in the same process and in memory, while others are handled by a RabbitMQ listener. This is another discussion: I'm thinking about just handling everything using RabbitMQ and throw away the other approach which is the root of the problem!Quickstep
For example, the problematic handler is the one that handles taggable objects. Whenever an object which implements a given interface (i.e. IHasTags... it's different than that, but it's enough to explain the case), there's a generalized domain event handler which checks if the tags need to be created. Since the more OOP-ish flow already uses ITagService, the handler can't depend on the whole service.Quickstep
So, there're three possible solutions in my mind: #1 is property injection (a workaround), yours and there's even a third approach: converting the domain event manager into a dependency root and locate the handlers from there, hence there would be no circular dependency tree anymore. But anyway, I'm not sure if it's really that critical that I mightn't be able to tag an object (since an exception in the middle of the entire flow wouldn't commit the unit of work), that's why I'm not absolutely sure if this concrete event should be handled by the service bus...Quickstep
Probably the service bus approach cuts down the problem by design, and just for the sake of avoiding the circular dependency and continue building my solution using good practices, it's worth the effort. I believe that your answer could be considered acceptable if you update it with the point of using durable queues. What do you think?Quickstep

© 2022 - 2024 — McMap. All rights reserved.