如何正确使用IoC容器



我对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中进行组合,并且应该通过IoC容器创建类A,它将自动解析依赖关系。IoC会在这里占据真正的优势吗?我仍然需要通过IoC集装箱,在那里我需要一些服务。优点是为所有服务传递单个参数,而不是传递多个参数。

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

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

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

使用依赖注入容器,您就拥有了包含所有实例的单个实体。把它发送到任何地方都很诱人,因为你会有完整的上下文,但我建议不要这样做。

在kangaru文档中,我在指南中添加了以下内容:

这个库是一个最小化耦合的好工具,但与这个库的耦合仍然是耦合的。

例如,如果void f();(作为一个自由函数(需要记录器,那么它应该作为参数传递。

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

现在这就是依赖注入库的作用所在。是的,你可以使用容器来获取里面的内容并将其发送给函数,但它可能是很多样板:

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

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

最好的办法是让图书馆尽量减少样板:

ioc.invoke(f);

kangaru容器具有invoke功能。您向它发送一个类似函数的对象或函数指针,它会自动注入参数。

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

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>> {};

现在,为非单个类定义这些服务可能看起来像样板文件,但如果您使用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不一定需要定义服务,只需将friend auto service_map(...) -> kgr::autowire添加到需要在其构造函数中提供服务的类中,即可使其与invoke一起使用

最新更新