本文来讲解下依赖注入,以及在C++编程中如何使用。本文通过一些概念讲述,例子分析带着大家先来明白什么是依赖注入,然后再来说明如何使用,以及目前比较规范或者说通用的使用方式,方便大家在代码编写或者框架搭建时使用,相对来说就比较简单实用
说到依赖注入我们首先来说下solid原则,solid原则是面向对象class设计的五条原则,分别是:
class Tools {};
class Human {
Tools tools;
};
这里我们就可以说Human这个类依赖于Tools,那么我们说的对于这个依赖项是从外部创建继而注入到这个类中。
class Tools {
public:
void doWork() {
std::cout << "do work" << std::endl;
}
};
class Human {
public:
Human(Tools& t) : tools(t) {}
void doWork() {
tools.doWork();
}
Tools& tools;
};
void MakeHuman() {
Tools t;
Human human(t);
human.doWork();
}
int main() {
MakeHuman();
return 0;
}
这个就是相对比较简单的通过构造函数进行依赖注入,这里使用构造函数来进行了依赖注,当然除此之外还可以通过一个setter的函数来实现注入。
可能大家看不出来有什么好处,我稍微改动一下代码就比较直观了。
class Tools {
public:
virtual void doWork() = 0;
};
class Hammer : public Tools {
public:
void doWork() override {
std::cout << "use hammer" << std::endl;
}
};
class Axe : public Tools {
public:
void doWork() override {
std::cout << "use Axe" << std::endl;
}
};
class Human {
public:
Human(Tools& t) : tools(t) {}
void doWork() {
tools.doWork();
}
Tools& tools;
};
void MakeHuman() {
Hammer hammer;
Human human1(hammer);
human1.doWork();
Axe axe;
Human human2(axe);
human2.doWork();
}
int main() {
MakeHuman();
return 0;
}
可以看到将Tools的函数声明为虚函数,让Hammer和Axe继承了Tools,然后Human根本不需要修改,这也就符合我们上边说到的依赖倒置原则,依赖的是doWork接口而不是某一个类,设想一下如果讲Tools的构造放到Human中,那是不是就是依赖了具体的实现类了。这样来说依赖注入对于解耦角度和扩展性都是很好优化。
通过以上内容相信大家都知道了所谓的依赖注入了,那么我们如何使用呢,大家可能会说上边已经有了使用的例子了。不过为了进一步的优化,同时也体现c++的优势,实现依赖注入并不是通过构造函数或者setter函数这种方式。而是使用一个容器,即IOC(控制反转)容器。
控制反转(Inversion of Control)是一种是面向对象编程中的一种设计原则,用来减低计算机代码之间的耦合度。其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。
使用某一个依赖的对象时,直接从容器中获取即可,更加减少了耦合性。提升了代码的整洁度。
class IX
{
public:
IX(){}
virtual ~IX(){}
virtual void g()=0;
};
class X : public IX
{
public:
void g()
{
std::cout << "it is a test" << std::endl;
}
};
void TestMyIoc()
{
//简单注册,需要类型信息和key
IocContainer ioc;
ioc.RegisterType("ss");
ioc.RegisterType("ss");//key重复,会报错
auto ix = ioc.ResolveShared("ss");
ix->g();
//简单注册,需要类型信息、接口类型和key
ioc.SimpleRegisterType("ff");
auto ix1 = ioc.ResolveShared("ff");
ix1->g();
}
这是ref4中的实现,可以看到首先构造了IocContainer的对象,X这个类继承了IX这个类,首先会注册X这个类的对象, RegisterType时会将X的对象构造步骤封装成函数存储起来,这样在使用的时候就可以通过ResolveShared来获取到,继而进行调用X对象的成员函数。第二个注册时可以声明注册的类的父类是什么,这样在ResolveShared的时候就可以通过依赖接口就能拿到对象了,而不需要依赖具体是那个类。
那么问题来了,IocContainer是如何实现的呢,大家可以直接跳转到ref4看源码,我这里简单讲述下:
class IocContainer : NonCopyable
{
public:
// ...
template
void RegisterType(string strKey)
{
typedef T* I;
std::function function = Construct::invoke;
RegisterType(strKey, function);
}
template
void RegisterType(string strKey)
{
std::function function = Construct::invoke;
RegisterType(strKey, function);
}
//...
private:
template
struct Construct
{
static I invoke(Ts... Args) { return I(new T(Args...)); }
};
void RegisterType(string strKey, Any constructor)
{
if (m_creatorMap.find(strKey) != m_creatorMap.end())
throw std::logic_exception("this key has already exist!");
m_creatorMap.insert(make_pair(strKey, constructor));
}
private:
unordered_map m_creatorMap;
};
首先作者是通过unordered_map来存储所有的数据的,且通过Any(C++17引入,不过这里的Any是作者自己实现)来做类型擦除,然后当调用RegisterType时,会将传入类型的构造封装成一个函数,进一步调用私有的RegisterType函数存入到m_creatorMap这个数据结构中。
再来看下resolve的实现:
class IocContainer : NonCopyable
{
public:
// ...
template
I* Resolve(string strKey)
{
if (m_creatorMap.find(strKey) == m_creatorMap.end())
return nullptr;
Any resolver = m_creatorMap[strKey];
std::function function = resolver.AnyCast>();
return function();
}
template
std::shared_ptr ResolveShared(string strKey)
{
auto b = Resolve(strKey);
return std::shared_ptr(b);
}
//...
};
可以看到Resolve也相当简单,仅仅就是从m_creatorMap找到相应的函数对象,并调用就能产生所需要的类对象了,ResolveShared再用shared_ptr包装一下。
使用IocContainer的好处相信大家也看到了,在需要创建的时候进行注册,然后在需要的时候直接获取即可,避免了很大程度的依赖。同时还可以通过接口(例子中的父类)来获取,又能够避免直接依赖于具体对象。
除了上边讲述到的IoC容器的一种表现形式,boost也有相关代码的提供(boost-ext/di),不过角度很新颖,同时也值得我们学习和使用。
我们看下使用的范例:
// 需要include这个头文件,di即为dependency inject(依赖注入)
#include
namespace di = boost::di;
class ctor {
public:
explicit ctor(int i) : i(i) {}
int i;
};
struct aggregate {
double d;
};
class example {
public:
example(aggregate a, const ctor& c) {
assert(87.0 == a.d);
assert(42 == c.i);
};
};
int main() {
const auto injector = di::make_injector(
di::bind.to(42),
di::bind.to(87.0)
);
injector.create();
}
我们看到,首先通过di::make_injector
来构造一个injector对象,并用它创建了一个example对象。很有意思的是,不需要用户自己去创建example依赖的aggregate和ctor对象。
这在一定程度上很完美的解决了耦合的这个问题,同时减少了用户去定义所依赖的对象。好处就是当所依赖的对象有变化(比如说构造函数的参数个数增加或者减少)时,这块代码根本不需要修改,这也完美体现了可扩展性。
那么他是怎么实现的呢?实现较为复杂,我们这里只是讲述基本原理。
首先当di::make_injector中传入参数,injector会将int值为42和double值为87.0先保存起来备用。
然后调用create函数,我们也简单跟踪下:
template ::value) = 0>
T create() const {
return __BOOST_DI_TYPE_WKND(T) create_successful_impl(aux::type{});
}
template
auto create_successful_impl__() const {
auto&& dependency = binder::resolve((injector*)this);
using dependency_t = typename aux::remove_reference::type;
using ctor_t = typename type_traits::ctor_traits__, T,
typename dependency_t::ctor>::type;
using provider_t = successful::provider;
auto& creatable_dept = static_cast&>(dependency);
using wrapper_t = decltype(creatable_dept.template create(provider_t{this}));
using create_t = referable_t>;
return successful::wrapper{creatable_dept.template create(provider_t{this})};
}
首先调用create函数,create最终调用到create_successful_impl__函数,create_successful_impl__也是比较关键的函数,其中ctor_t这个类型就会将构造的这个类所依赖的对象解析出来。我使用gdb把ctor_t这个类型打印出来了:
type = struct boost::ext::di::v1_3_0::aux::pair, boost::ext::di::v1_3_0::core::any_type_ref_fwd > > > [with T1 = example, T2 = boost::ext::di::v1_3_0::aux::pair, boost::ext::di::v1_3_0::core::any_type_ref_fwd > >] {
typedef boost::ext::di::v1_3_0::aux::pair type;
}
这一坨看着太复杂了,我们把命名空间省略下:
type = struct aux::pair, core::any_type_ref_fwd > > >
[with T1 = example, T2 = aux::pair, core::any_type_ref_fwd > >]
{
typedef aux::pair type;
}
可以看到这个type其实是一个pair,pair的第一个元素就是这个类(example),然后第二个元素就会解析出来这个类构造函数依赖的类型。direct是内置类可以忽略,接下来的两个类就是依赖的类型了。不过大家很神奇,他这个也没解析出来呀。boost这里使用了一个技术手段,叫做any_type,顾名思义就是可以匹配任意类型。
我用一个简单的例子带着大家来了解这个技术:
struct AnyType {
template
operator T() {
return T{};
}
};
struct Test1 {
int a = 8;
};
class Test2 {
public:
explicit Test2(Test1 t1, int) : t1_(t1) {
std::cout << t1_.a << std::endl;
}
Test1 t1_;
};
int main() {
Test2(AnyType(), 10);
}
我们看到AnyType的重载了()
操作符,但是又是一个模板,当在调用时又不指明这个模板参数是什么,让编译器去推断。这里编译器就会推断出来这个T是Test1。
同样core::any_type_ref_fwd
就是一个any_type的类,然后使用其构造一个对象传入,进而就可以构造出来example对象了。 如果匹配的这个类型是int或者double就会用最开始保存的数据进行赋值。
关于boost/di大致就是这个实现思路,当然内部实现还是很复杂的,包括使用name来匹配,多个相同类型等等实现。
那么boost/di实现的其实算是一个IOC的容器工厂,更加简便了IOC的使用负担,帮助我们来构造出来所需要的对象,也不需要额外的配置什么的,还是比较完美的。
不过boost/di其实只能依赖构造时把依赖的对象赋值,不能够在随处取用,这个角度来讲实例一就是比较方便了,使用方式还是看大家的使用场景了。
本文简述了依赖注入的概念,然后使用C++实例来讲述如何使用依赖注入,接着又给大家展示现代C++如果使用IOC来实现依赖注入。同时也比较boost/di和IOC容器的异同。