C++与依赖注入

本文来讲解下依赖注入,以及在C++编程中如何使用。本文通过一些概念讲述,例子分析带着大家先来明白什么是依赖注入,然后再来说明如何使用,以及目前比较规范或者说通用的使用方式,方便大家在代码编写或者框架搭建时使用,相对来说就比较简单实用

什么是依赖注入

说到依赖注入我们首先来说下solid原则,solid原则是面向对象class设计的五条原则,分别是:

  • S 单一职责原则
  • O 开闭原则
  • L 里氏替换原则
  • I 接口隔离原则
  • D 依赖倒置原则
    那与我们相关的也是依赖倒置原则,描述为我们的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容器的异同。

ref

  1. https://www.youtube.com/watch?v=IKD2-MAkXyQ
  2. https://www.freecodecamp.org/chinese/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it/
  3. https://www.jianshu.com/p/07af9dbbbc4b
  4. https://www.cnblogs.com/qicosmos/archive/2013/04/22/3035074.html
  5. https://github.com/boost-ext/di
  6. https://boost-ext.github.io/di/index.html

你可能感兴趣的:(C++,base,c++,算法,boost,di,ioc)