1

I'm building a console app (A), which depends on library B. Library B further depends on library C. I am building all 3 entities. Most of the development completed so far has been done using a functional design and so far this is working well, and is currently stateless, and no instantiations have been needed so far. I am now at a point where I need to declare some dependencies and inject them as appropriate. The interaction between libraries B and C should be transparent to the application A. I have been reading up on dependency injection/inversion of control and I've decided that I will use inversify as the dependency injection framework. One point I am 'stuck' on is the notion of Composition Roots and where they should be created. I found this article 'https://blog.ploeh.dk/2011/07/28/CompositionRoot/' which states that the CR should be as close to the main entry point of the application. It also says that CRs, are not applicable to libraries just applications. But the interaction between libraries B & C is an implementation detail that the client should not be aware of. So what is the correct resolution to this issue? I'm inclined to define a CR in library B so that its dependency on a component in library C can be injected. This seems correct to me, but it does seem at odds with established wisdom. To quote the article: "Only applications should have Composition Roots. Libraries and frameworks shouldn't.". It seems that to do this correctly, the client app needs to be aware of all the implementation crud and its up to the client to compose everything via a Composition Root.

But if library B needs a dependency to be injected into it (which comes from library C), how do we do this? I'm now suffering analysis paralysis ...

PS, I also considered this article: Design/Architecture for passing cross-cutting parameters via constructors at composition roots?, but that does not address the issue of the application/client being aware of downstream dependencies.

1 Answer 1

2

That article has two subtly different general recommendations:

1) "Only applications should have Composition Roots. Libraries and frameworks shouldn't."

2) "A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container."

You said:

It seems that to do this correctly, the client app needs to be aware of all the implementation crud and its up to the client to compose everything via a Composition Root.

Now, I know that one of the issues here is that it looks like the (human) users of your library must know about and understand C in order to use B. That's not necessarily the case. But let me digress a bit, just so that we're on the same page when it comes to DI.

Think about the client application and the Composition Root as two separate components (even though they may reside in the same module/package/library), and design them like that. The Composition Root references (knows about) everything that is being composed (actually, that very fact significantly contributes to the decoupling of other components). It is intended to be the single place where the entire application is composed. The core application itself doesn't need to know anything about C, because C will be injected into B by the Composition Root. That looks something like this (some arrows are omitted - the CR knows about the interfaces as well):

CR

Note that B and C are decoupled in the sense that C is (in this particular design) swappable. Having the application's Composition Root compose B and C is more flexible, as it lets clients easily swap C for a different implementation (perhaps even one developed by a 3rd party). It also lets you test each of these components in isolation. Now, sometimes it may be the case that C is really not that useful on it's own, and that may render these considerations somewhat less relevant.

1) "Only applications should have Composition Roots. Libraries and frameworks shouldn't."

I think this statement is motivated, at least in part, by what I described above.

But, in another article, he writes about how to design a DI-friendly library; there's a section named "Consider a Facade":

If some objects are difficult to construct, because their classes have complex constructors, consider supplying a Facade with a good default combination of appropriate dependencies.

In your case, this fits nicely with the idea that clients of B shouldn't have to know about or understand C in order to get started. So having a convenience Facade lets you be both flexible and have one or more preconfigured defaults (in your case, there's only one option at the moment). Note that this Facade, as described in the article, actually composes the components of the library in the style of what Seemann calls Pure DI, so you could argue that it's a local composition root, but it looks like he does not consider this to be the Composition Root pattern.

It looks like it's a conceptual thing; I'm guessing now, but it seems to me that his definition of Composition Root is that it's a dedicated single place where something (potentially, a complicated graph of dependencies) is assembled as a whole, and as such, it isn't really meant to be configurable at runtime by client code (so, it's not like a DI container, although it may use one; it basically produces an assembled thing based on a spec and/or a convention). Any interface you can think of to provide this kind of flexibility to clients is going to be either less powerful or more complicated then just letting the client itself assemble the dependencies. So if a library (or a framework) has a Composition Root and just returns the end result (the composite), most (or all) of that decoupling and modularity is lost from the perspective of the client. Again, this is an impression I get from the way he writes about it, but maybe I'm wrong.

Regardless, the bottom line is, it's OK to provide reasonable defaults for your library, but it's a good practice to do it in a way that lets clients inject dependencies of their choice (their own "C") if they wish to do so. This makes both B and C more reusable (and likely more useful to other people, as those libraries will more easily support scenarios that you haven't anticipated during development). Using an internal DI Container to provide those defaults is probably an overkill in many cases.

As for the other statement:

2) "A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container."

In his book, right before this line, Seemann talks about how we should be careful not to misuse the (single) DI Container as a Service Locator. I'm under the impression that that's the primary motivation behind this recommendation. A Service Locator is effectively a global object that classes can use internally to request their dependencies anywhere in the codebase (more or less), and this can turn out to be bad in various ways.

There are some other concerns related to maintainability of the code. If you open the article you referenced, and scroll a bit down, there's a comment where Seemann writes:

The problem with letting each module register their own types to any sort of container is that it tightly couples the modules (and any application in which you might want to use them) to a specific container. Even worse, if you ever need to consume a service provided by one module from another module, you'd have to couple these two to each other. Pretty soon, you'll end up with a tightly coupled mess.

However, this comment is a bit handwavy, as there is a great deal here that depends on exactly how you do this. For example, the aforementioned Facade also couples a library to its dependencies, but the coupling is confined to the Facade; the core component of the library stays decoupled. These concerns are a bit more high level, and can become an issue in an evolving codebase, where you need to carefully control the dependencies and remodel things in order to support those changes and manage complexity, and where you need to effectively communicate the design/architecture to other developers working on the same system.

1
  • Hi Filip, first let me say, thank you very much for your eloquent and comprehensive answer. You have helped me to clear up a few confusions I had and actually, you've made a very compelling argument about giving the client the option about how dependencies are composed which now seem to make more sense to me and has changed my thinking in this regard. I still have to do a lot more reading but this has set me on the right path for sure...
    – Plastikfan
    Jun 19, 2019 at 10:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.