boost-ext / di

C++14 Dependency Injection Library
https://boost-ext.github.io/di
1.13k stars 136 forks source link

Question: Creating objects at runtime without direct injector access #518

Open englercj opened 3 years ago

englercj commented 3 years ago

Hello, I have what seems like a trivial case that I just cannot figure out the right pattern for. Basically I have an application with 'Documents'. When certain actions happen in the app, I create new Document objects. I use dependency injection to make it easy to prepare these objects.

You can imagine it is this:

class IDocument {};

class Document1 : IDocument {};
class Document2 : IDocument {};

I create an injector in my main and use that to create the Application object that drives the entire application. So once I'm in application code, I no longer have access to the injector itself.

Something like this:

int main(int argc, char* argv[])
{
    const auto injector = boost::di::make_injector(
        boost::di::bind<Allocator>.to<DefaultAllocator>()
    );

    Application& app = injector.create<Application&>();
    return app.Run(argc, argv);
}

I don't lean too heavily into the Interface->Impl binding, instead just use DI to make getting access to my various pieces easy. So everything uses the implementation as constructor parameters which are injected fine without any binding necessary.

Eventually in my code I'll want to create a Document1 or Document2 instance, but I'm not sure what the right pattern to do so is.

A factory is almost what I want but it requires me to bind and accept a factory for each type I want to create. Assuming I understand correctly, I'd have to do this:

// in main
const auto injector = boost::di::make_injector(
    boost::di::bind<Allocator>.to<DefaultAllocator>(),
    boost::di::bind<boost::di::extension::ifactory<Document1>>().to(di::extension::factory<Document1>{}),
    boost::di::bind<boost::di::extension::ifactory<Document2>>().to(di::extension::factory<Document2>{}),
);

// then later:
class Workspace
{
    Workspace(
        const boost::di::extension::ifactory<Document1>& doc1Factory,
        const boost::di::extension::ifactory<Document2>& doc2Factory)
    {
        // I'd probably store the factory, and do this elsewhere, but you get the idea.
        std::unique_ptr<Document1> p = doc1Factory.create();
    }
};

But there are a lot of these classes I'd like to make at runtime, and have benefit from dependency injection. Having to bind a factory for each concrete class is just not reasonable at scale. It also means anyone making a new Document need to remember to open main and add it to a gigantic list of Documents/Panels/Dialogs/etc so it gets wired through. Another drawback is that I need to include the headers for the concrete document types in the headers of classes that create them, because I have to take an ifactory<Document1> as a param.

Instead I'd love to have a factory where I can choose what to create as a template param, like this:

class Workspace
{
    Workspace(const ifactory<IDocument>& docFactory)
    {
        // This would let me create any concrete I want from a single factory, and ideally would do static checking
        // to ensure that the template param does indeed extend from IDocument.
        std::unique_ptr<Document1> p = docFactory.create<Document1>();

        // struct NotADocument {};
        // auto p = docFactory.create<NotADocument>(); // this wouldn't compile
    }
};

If I need to make a binding for each type of thing I want to inject this way, that is way less work than having a factory for each concrete I want to create.

I've done this in the past by hacking my injector into the global scope and writing wrappers that do the right thing, but I'd like to know if there is some way to accomplish this within the DI ecosystem without globally exposing my injector.

Thanks!

jevansio commented 2 years ago

Hi I don't know if this helps and i understand the smell of a service locator, but I have been banging my head all weekend trying to solve a similar issue. Tickets #319, #453, #225 & #213 all seemed related and provided either nuggets of info or red herrings but none a complete solution, until this morning when I knocked up this:

    template <class T = void>
    class servicelocator
    {
        public:
        using boost_di_inject__ = inject<self<T>>;

        template <typename C, class... TArgs>
        C create(TArgs&&... args)
        {
            _injector.install(bind<TArgs>().to(std::forward<TArgs>(args))[override]...);
            return _injector.template create<C>();
        }

        template <class TInjector>
        explicit servicelocator(const TInjector& i) noexcept
        {
            _injector.install(i);
        }
    private:
        di::extension::injector<> _injector;
    };

I've left the templated T parameter on it (even though its not used) as I plan to restrict what types it can create using its declaration, otherwise it truly can create anything :/

These 3 classes co-operate with C creating both A and E, A having a dependency in the container, E having a dependency specified at the point of creation

class ClassA
{
public:
    ClassA(double d)
    { 
    ...

class ClassE
{
public:
    ClassE(int i)
    {
    ...

class ClassC
{
public:
    explicit ClassC(di::extension::servicelocator<> d)
    {
        d.create<ClassA>(); // all dependenicies resolved by the container
        d.create<ClassE>(1); // explicit dependencies for things not in the container
    }
};

From a composition root of:

        auto typed_injector = di::make_injector(
            di::bind<double>().to(2.14159)
            );

        auto c = typed_injector.create<ClassC>();

The integer i is filled in from the variable arguments specified at the point of creation where as anything else is resolved from the di container (ie the double)

Please any comments welcome on the correctness of the utility code

P.S. huge hats off to @krzysztof-jusiak and all the contributors this really is a fantastic achievement to get c++ to do what C# does so easily!

J

jevansio commented 2 years ago

EDIT - Damn, it only seems to work 1 level deep, as soon as ClassA or E take a dependency on the service locator it fails to compile, I wonder if the lazy<> I based it on does the same, I think I'm coming to the limit on my ability to come up with a solution to the problem of objects creating other objects (with deps) who's type is not controlled by the container

englercj commented 2 years ago

For reference, here is what I ended up with to make my injector globally available:

    inline auto MakeAppInjector()
    {
        return di::make_injector(
            /* ... bindings ... */
        );
    }

    using AppInjectorType = decltype(MakeAppInjector());

    extern const AppInjectorType* g_appInjector;

    template <typename T>
    auto DICreate() -> decltype(g_appInjector->template create<T>())
    {
        return g_appInjector->template create<T>();
    }

Later in my main I make and store the injector:

int main()
{
    const auto injector = MakeAppInjector();
    g_appInjector = &injector;

    // ... do app stuff with injector

    return 0;
}

Then I can use DICreate<T>() anywhere to create what I need. I opened this issue to see if I can delete this and do something better, unfortunately haven't found anything yet :(

jevansio commented 2 years ago

Thank you so much for your example.

I presume with AppInjectorType being an app defined that this utility needs copy/pasting to each new app you write?

I think during this whole process it slowly dawned on me that this framework works best in compile time mode and trying to engineer runtime logic into it was like expecting to find the solution to a perpetual motion machine

englercj commented 2 years ago

I presume with AppInjectorType being an app defined that this utility needs copy/pasting to each new app you write?

Yup. I have this file copy-pasted into a few different apps where I use dependency injection :)