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.

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206

1 Answers1

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.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • 1
    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... – Matías Fidemraizer Apr 18 '17 at 13:12
  • "what kind of architecture can produce this approach in the long term". That would be a [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) or any architecture that is based on the SOLID principles. – Steven Apr 18 '17 at 13:14
  • 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: – Matías Fidemraizer Apr 18 '17 at 13:17
  • Actually, the domain event handlers are injected as an `IList` 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. – Matías Fidemraizer Apr 18 '17 at 13:22
  • 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. – Matías Fidemraizer Apr 18 '17 at 13:26
  • 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#). – Steven Apr 18 '17 at 13:31
  • 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.\ – Steven Apr 18 '17 at 13:34
  • 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! – Matías Fidemraizer Apr 18 '17 at 13:47
  • 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. – Matías Fidemraizer Apr 18 '17 at 13:51
  • 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... – Matías Fidemraizer Apr 18 '17 at 13:56
  • 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? – Matías Fidemraizer Apr 18 '17 at 14:02