Open petertflem opened 4 years ago
Hi @petertflem,
The information you are looking for is indeed missing.
On page 68 we describe the following fix:
This means that to fix the violation, we need an additional two extra projects in our solution—one for the isolated UI layer without the Composition Root and another for the
IProductService
abstraction that the UI layer owns.
After revisiting this section again, I now realize that the text is confusing, because a more convenient solution is to extract the service implementations to a different project, instead of extracting the service abstractions.
To show this in action, I created this commit for you on this branch.
It adds two new projects:
The projects now have the following dependencies:
IProductService
).IProductService
).IProductService
)I hope this answers your question.
Steven
Hi, @dotnetjunkie!
Thank you so much for your time and reply!
The way I understood the problem was that the Dependency Inversion Principle states abstractions should be shipped with their owning modules, and thus IProductService
should be shipped with the UI layer.
I've drawn out boxes and arrows of the solution you mention above, and I need some extra help understanding it.
IProductService
still lives in a project owned by the domain; Commerce.Domain. Does that mean it still violates the DI Principle? IProductService
was defined in Commerce.Web.Presentation, which is the consumer, then replacing the UI layer would break the implementation of the interface. ProductService
would no longer find the interface it's implementing. Is this the desired outcome?IProductService
, like it originally did. I don't quite understand the benefits of moving the implementations into their own project versus having them where they were. How does this improve the loosley coupled design? How is this different from before?Again, thank you so much for your time. The book so far has touched on subjects that are relevant nearly every day at work.
Kind regards Peter
Hi,
I think it's best to forget about my previous example. I cooked this example up to quickly, and you are right: it doesn’t solve the DIP violation at all. And here's a TLDR; IMO you shouldn't try to solve this violation, because in most applications there will be little to no benefits. It just adds complexity.
The way I understood the problem was that the Dependency Inversion Principle states abstractions should be shipped with their owning modules, and thus IProductService should be shipped with the UI layer.
That's indeed what the DIP prescribes.
The IProductService still lives in a project owned by the domain; Commerce.Domain. Does that mean it still violates the DI Principle?
You are correct; the DIP is still violated in the example.
If the IProductService was defined in Commerce.Web.Presentation, which is the consumer, then replacing the UI layer would break the implementation of the interface. ProductService would no longer find the interface it's implementing. Is this the desired outcome?
This is typically not a desired outcome, as you usually want the domain layer to be the center of your universe. Coupling it to (one specific) UI makes it quite hard to reuse that domain layer in a different context (for instance background processing or using a different UI).
The UI layer is still depending on the domain layer for IProductService, like it originally did. I don't quite understand the benefits of moving the implementations into their own project versus having them where they were. How does this improve the loosely coupled design? How is this different from before?
What I did in the repo is splitting the definition of domain objects and abstractions from the behavior that is part of the domain. This, for instance, prevents the presentation layer to accidentally depend on a service implementation. So it enforces that the Presentation layer depends on abstractions, not implementations. But apart from that, there is little to gain here.
If two different projects wants to consume the same interface, and if it should be placed with the consumer, with which consumer should it be placed? In this scenario, it seems neat to keep it in the domain layer, even though it violates the DI Principle. It would make it accessible to all. Or is the point that there won't be two equals interfaces, because use cases will likely be at least slightly different?
In case you have multiple consuming modules/assemblies, Robert C. Martin states in Agile Principles, Patterns, and Practices:
there are times when we don’t want the server to depend on the client […] In that case, the clients must agree on the service interface and publish it in a separate package.
Whether consumers can share the interface at all, depends on whether they tend to achieve the same thing (same use case) or not, as you already noticed.
I would also be very grateful if you could touch on why we need a UI layer isolated from the Composition Root to achieve the DI Principle in this scenario. I like the idea of splitting out the Composition Root into its own service so that I can easily reuse it if I remove the current UI layer. But, I'm not sure why it was needed in this scenario.
Having a separate project for the presentation objects prevents that project from requiring a dependency on the data access layer. The project that contains the Composition Root needs a dependency on all projects in the system, because it needs to compose its classes. Typically, however, I keep the presentation layer and Composition Root in the same project, because I don't mind having that project dependency. I just make sure that my presentation objects depend on abstractions, not implementations.
Another reason for splitting Composition Root from presentation is to prevent cyclic dependencies. This would happen, for instance, if you place the IProductService
into the presentation layer. In that case the DL needs to depend on the presentation layer, but that project already depended on the DL. That won't compile.
But as I see it now, there are two ways of fixing this DIP violation:
IProductService
interface (and its DiscountedProduct
data class) to an "interface" project managed by the presentation layer, e.g. Commerce.Presentation.Abstractions
. Both Domain layer and Presentation Layer can depend on it.IProductService
interface (and DiscountedProduct
) to the Presentation Layer, but don't let ProductService
implement that interface at all. Instead, inside the Composition Root, create an adapter for the IProductService
interface and that depends on the ProductService
implementation, which maps from the returned Domain Layer object to the IProductService
's DiscountedProduct
object. Problem with this approach is that it causes a lot of adapter code being written, all with their boilerplate mapping.But here's an important point about DIP and SOLID principles: They are not a means to an end. You apply them in context and being 100% SOLID is not only undesirable, but also an unreachable goal. There will always be violations, but the trick is to manage them and fix the ones that impact maintainability. And as you already questioned, is this strict enforcement of the DIP worth the trouble in this case? I find it unlikely to be so. The Domain layer is the core of your application. Preventing the presentation layer from having a dependency on the domain is hardly ever very practical. What would be left of your UI without a domain. It's always built for a specific domain. There's nothing really to abstract. Mark and I might have mentioned this somewhere in the book, but I'm not sure where. But we did mention that out of pragmatism, we decided not to go through the extra mile of fixing this violation. But as you might see now, this wasn't only a practical solution to save some space and complexity in the book, I think it's a practical solution for most applications.
I hope this helps.
Hi, @dotnetjunkie,
Thank you so much for the elaborate reply! It helped clear things up, and I will keep this in mind. It is very informative.
Thanks again, truly appreciated.
Have a wonderful weekend!
Peter
I kind of doubt if this belongs here, but I though I'd try.
On page 68, you mention a violation of the dependency inversion principle. A solution to fix it is also described, but I can't quite understand it. Would it be possible to add some code showing the solution described in the book? It would make it easier to wrap my head around it.
Of course, only if you have the time. I fully understand and respect that you might be very busy.
Thank you so much for a good read this far, just finished part 1!