DevTeam / Pure.DI

Pure DI for .NET
MIT License
506 stars 22 forks source link

Class library usage example #2

Closed thargy closed 2 years ago

thargy commented 2 years ago

Hi,

Nice project. I've read through and I'm not clear on one quite common use case.

Let's say I'm creating a class library that wants to expose a utility method to inject dependencies into a container, how would you recommend achieving that?

For example, my library might expose a number of interfaces with concrete implementations, and want to expose an extension method like:

    public static class DIExtensions
    {
        public static IConfiguration UsingLib(this IConfiguration configuration) => configuration
            .Bind<IA>.To<A>()
            .Bind<IB>.To<B>();
    }

Which you would use like:

        private static void Setup() => DI.Setup()
            .UsingLib()
            .Bind...

However IConfiguration is internal, so you can't even do this kind of thing even in the same project.

I understand that the point of DI is that the classes (in my example 'A' and 'B') don't need to know about each other, and that Pure.DI "isn't a framework", however, it is often useful to have a chunk of 'common usage' code for convenience to prevent a lot of boiler-plate code needing to be added each time.

NikolayPianikov commented 2 years ago

Hi,

I am very glad that this project is interesting for you.

This was done on purpose. The main idea is to write code in pure DI style, where there are no containers and no other things which might make a solution messy. I like the idea of Composition Root which has a good definition in the book "Dependency injection in .NET" by Mark Siman. In chapter 3.1.1 "Composition Root" He wrote:

When you write loosely coupled code, you create many classes that you must compose to create an application. It can be tempting to compose these classes a little at a time in order to create small subsystems, but that limits your ability to INTERCEPT those systems to modify their behavior. Instead, you should compose classes all at once. COMPOSITION ROOT is a (preferably) unique location in an application where modules are composed together. The COMPOSITION ROOT can be spread out across multiple classes as long as they all reside in a single module. When you look at CONSTRUCTOR INJECTION in isolation you may wonder, doesn’t it just defer the decision about selecting a dependency to another place? Yes, it does, and that’s a good thing; this means that you get a central place where you can connect collaborating classes. The COMPOSITION ROOT acts as a third party that connects consumers with their services.

So it is better that only a single place in an application (entry point) should know about how to compose an object's graph. This module has references to all types including interfaces and implementations.

On another hand, sometimes you are needed to have owned DI for some library which will be used separately and by itself. In this case, I would prefer to set up a composition root (or several composition roots) within this library internally IN SINGLE MODULE and expose several public static methods to create this root outside. You could use these static methods in binding further in new composition roots. And I would prefer do not share information on how to build the composition root of this library outward because this is quite sensitive information. And you cannot change this in the future without breaking the compatibility of this library with its consumers.

You can reuse "binding hints" using this approach.

But I suppose it is possible to implement your requirement. And it is not needed to make IConfiguration public because we have a full syntax and semantic model of all dependencies on the build stage. The only thing that stops me is that it might be used to "shoot oneself in the foot".

thargy commented 2 years ago

Thanks @NikolayPianikov, that's a reasonable answer; and I understand the reasoning. There's always a fine line between theory and practice, it's why mathematicians and engineers are a very different skill set!

So here's a practical example.

You have a game, with the majority of code common (>90%), however, certain functionality is device-dependent (e.g. file logging, input-output, windowing, etc.)

You have a build for each device (e.g. *nix, Windows, Mac, iOS, Android). The majority of the common code is initialised in the same way, however, that code is dependant on device-specific interfaces (IO, clock, rendering, etc.) and the device-specific code will also want injections of library code.

Reproducing all the code for initialising that common library in each calling program is one of the 7 Forbidden Programming Paradigms Copy-Paste Programming.

Would your recommendation be to parameterize a static Setup method in the library with required dependencies, e.g.

    public static class MyLibrary
    {
        public static MyLibrary Setup(ILoggerFactory loggerFactory, ...) => DI
            .Bind<ILoggerFactory>.As(Singleton).To(_ => loggerFactory)
            ...
    }

This would allow you to inject the dependencies into the library, but would then require lots of boiler code in the caller to grab dependencies from MyLibrary...

NikolayPianikov commented 2 years ago

There's always a fine line between theory and practice ...

I agree with this statement. And I understand a general scenario and the related problem. This API is not suitable:

Setup(ILoggerFactory loggerFactory, ...) => DI
            .Bind<ILoggerFactory>().As(Singleton).To(_ => loggerFactory)

because this code is not actually working at runtime :( Pure.DI gathers hints from it at the build stage. Because the composition graph is already defined and compiled at the runtime stage, it should select OS implementation manually at runtime using some logic. For instance, you could use a "tag" to define binding for each OS and add binding without a tag and a logic to switch between OS. Please see this sample. And another way is to use this approach to make several static methods to create appropriate composition root for each OS.