如何正确使用 IoC 容器?

How to properly use IoC container?

我对 IoC 的想法还很陌生,我正试图跳过服务定位器模式。 我选择了 Kangaru 实现。假设我想在我的应用程序的不同地方使用音频和记录器服务。我现在拥有的:

    #include <kangaru/kangaru.hpp>
    #include <iostream>
    using namespace std;

    struct IAudio {
        virtual void playSound(int id) = 0;
    };

    struct Audio : IAudio {
        void playSound(int id) { cout << "Playing " << id << '\n'; }
    };

    struct IAudioService : kgr::abstract_service<IAudio> {};
    struct AudioService : kgr::single_service<Audio>, kgr::overrides<IAudioService> {};
    
    struct ILogger {
        virtual void log(const std::string& message) = 0;
    }; 

    struct Logger : ILogger {
        void log(const std::string& message) { cout << "Log: " << message << '\n'; }
    };

    struct ILoggerService : kgr::abstract_service<ILogger> {};
    struct LoggerService : kgr::single_service<Logger>, kgr::overrides<ILoggerService> {};

    struct User {
        User(kgr::container& ioc) : ioc_{ioc} {}
        kgr::container& ioc_;

        void f1() { ioc_.service<IAudioService>().playSound(1); }
        void f2() { ioc_.service<ILoggerService>().log("Hello"); }
    };

int main()
{
    kgr::container ioc;
    ioc.emplace<AudioService>();
    ioc.emplace<LoggerService>();
    User u(ioc);
    u.f1();
    u.f2();

    return 0;
}

如果我没理解错的话,此时它只是一个服务定位器,不是吗? 但是如果我有一些嵌套结构,像这样:

    struct A {
        B b;
    }
    struct B {
        C c;
    }
    struct C {
        D d;
    }

,应该在某个Composition Root中组合,class A应该通过IoC-container创建,会自动解析依赖关系。 IoC 将在这里发挥真正的优势,对吗?而且我仍然必须在任何需要服务的地方传递 IoC-container。优点是为所有服务传递单个参数而不是传递多个参数。

还有一件事:依赖注入以同样的方式为自由函数工作;如果我想在某些 void f() 中使用记录器,我应该通过参数传递 IoC 容器,或者直接在内部使用 - 在这种情况下没有依赖注入。但如果我不想让参数列表混乱,我别无选择。

使用库处理依赖注入的主要优点是:

  • 样板代码的自动化
  • 有一个包含当前上下文实例的中心位置

使用依赖项注入容器,您拥有包含所有实例的单个实体。将那个东西发送到任何地方可能很诱人,因为你将拥有可用的整个上下文,但我建议不要这样做。

在袋鼠文档中,我在指南中添加了这个:

This library is a great tool to minimize coupling, but coupling with this library is still coupling.

因此,例如,如果 void f();(作为自由函数)需要记录器,则应将其作为参数传递。

void f(ILogger& logger) {
    // ...
}

现在这就是依赖注入库的用武之地。是的,您可以使用容器获取内部内容并将其发送到函数,但它可能有很多样板文件:

f(ioc.service<ILogger>());

与您的用户类型相同,您将容器用作包含上下文的单个事物,而不使用它的样板减少功能。

最好是让库最小化样板文件:

ioc.invoke(f);

袋鼠容器具有invoke功能。你向它发送一个像对象或函数指针这样的函数,它会自动注入参数。

User class 也是如此。最好是在构造函数中接收必要的东西:

struct User {
    User(ILogger& l, IAudio& a) : logger{&l}, audio{&a} {}

    ILogger* logger;
    IAudio* audio;

    // You can use both logger and audio in the f1 and f2 functions
};

当然需要制作User一项服务,但不是单一的:

struct UserService : kgr::service<User, kgr::dependency<ILoggerService, IAudioService>> {};

现在,为非单一 classes 定义这些服务可能看起来像样板文件,但如果您使用 kangaru 的服务地图等新功能,可以通过多种方法显着减少它:

// Define this beside the abstract services
// It maps A parameter (ILogger const& for example) to a service
auto service_map(ILogger const&) -> ILoggerService;
auto service_map(IAudio const&) -> IAudioService;

然后,您可以将服务声明为单行,使用服务映射实用程序生成服务:

struct User {
    User(ILogger& l, IAudio& a) : logger{&l}, audio{&a} {}

    ILogger* logger;
    IAudio* audio;

    // You can use both logger and audio in the f1 and f2 functions

    // Map a `User const&` parameter to a autowired service
    friend auto service_map(User const&) -> kgr::autowire;
};

然后,您可以为生成的服务命名:

// Optional step
using UserService = kgr::mapped_service_t<User const&>;

现在终于可以使用容器生成实例了:

// Creates a new instance, since it's a non single
User newUser1 = ioc.service<User>();

// Or use a generator:
auto userGenerator = ioc.service<kgr::generator_service<UserService>>();

User newUser2 = userGenerator();
User newUser3 = userGenerator();

// Or use invoke:
ioc.invoke([](User user) {
    // user is generated instance
});

ioc.invoke([](kgr::generator<UserService> gen) {
    User newUser1 = gen();
    User newUser2 = gen();
});

如您所见,使用 invoke 不一定需要定义服务,只需在 class 中添加 friend auto service_map(...) -> kgr::autowire 即可,该 class 需要在其构造函数中使用服务,才能与 [=20= 一起使用]