繁体   English   中英

通过构造函数进行依赖注入的最佳实践

[英]Best practices for dependency injection via constructor

控制反转是一种价值证明技术,用于模块化系统并使组件彼此解耦。

低耦合始终是一个优势:它简化了组件的自动测试,并使代码更符合 单一职责原则

在声明对另一个 class 的依赖项的方法中(服务定位器、调用公共方法的属性注入/设置公共属性...),构造函数注入似乎是最好的方法。

尽管它可能是最难实施的一个(至少在列出的三个中),但它具有显着的优势:

  • 通过构造函数签名,所有依赖项都真正可见;
  • 由于定义明确的实例化顺序,不会发生循环依赖。

C++ 提供的通过构造函数执行注入的多种选择的优缺点是什么?

实例可复制类

class object
{
public:
  object(dependency d) : dep_(d) {}

private:
  dependency dep_;
};

仅适用于dependency类完全无状态的情况,即没有任何成员。 实际上,这种情况很少发生,因为dependency类可能存储自己的依赖关系。

原始指针

class object
{
public:
  object(dependency *d) : dep_(d)
  {
    if (d == nullptr)
      throw std::exception("null dependency");
  }

private:
  dependency *dep_;
};

这就像真正的注射一样。 我们需要检查传递的指针是否为nullptr值。

object类不拥有dependency类,因此调用代码以确保在dependency对象之前销毁object是责任。

在实际应用中,有时很难验证。

参考

#define DISALLOW_COPY_AND_ASSIGN(Class) \
  Class(const Class &) = delete;        \
  Class &operator=(const Class &) = delete

class object
{
public:
  object(dependency &d) : dep_(d) {}

  DISALLOW_COPY_AND_ASSIGN(object);

private:
  dependency &dep_;
};

引用不能为空,因此在此预期中更安全一些。

但是,这种方法会给object类带来额外的约束:它必须是不可复制的,因为无法复制引用。 您必须手动覆盖赋值运算符和复制构造函数以停止复制或从boost::noncopyable继承它。

与原始指针一样,所有权约束已到位。 调用代码应为两个类提供正确的销毁顺序,否则引用将变为无效,应用程序将因访问冲突而崩溃。

如果依赖项是const引用:

class object
{
public:
  object(const dependency &d) : dep_(d) {}

private:
  const dependency &dep_;
};

你应该注意object类接受对临时对象的引用这一事实:

dependency d;
object o1(d);             // this is ok, but...

object o2(dependency());  // ... this is BAD.

更多详情:

智能指针

class object
{
public:
  object(std::shared_ptr<dependency> d) : dep_(d)
  {
    if (!d)
      throw std::exception("null dependency");
  }

private:
  std::shared_ptr<dependency> dep_;
};

与原始指针类似,但所有权由智能指针机制控制。

仍然需要在构造函数体中检查nullptr

主要优点是dependency对象生存期控制:调用应用程序不需要正确控制销毁顺序(但考虑到在使用std::shared_ptr设计API时需要非常小心 )。

一旦不再使用dependency类,它就会被shared_ptr析构函数自动销毁。

有些情况下, shared_ptr拥有的对象不会被销毁(所谓的循环引用 )。 但是,使用构造函数注入时,由于特定的明确定义的构造顺序,循环依赖性是不可能的。

如果在整个应用程序中没有使用其他注入方法,这当然有效。

智能指针的开销很小,但在大多数情况下并不是真正的问题。

更多详情:

这是一个老问题,但对我来说这是一个热门话题,因为我在所有 web 我能听到的框架中都发现了自动依赖注入魔法,它们通常是用内省的恶作剧构建的,我总是很高兴发现它们的实现。 但是我在 C++ 中找不到一个简单的方法来做同样的事情。

服务定位器方法确实可以很好地解决这个问题,但是在构造函数中声明依赖关系并在两者之间摆脱这种模式似乎更干净,使用起来更灵活,因为它更容易实例化您的类,传递不同的服务实例。

但是服务定位器方法也可以处理循环依赖,因为它们可以被懒惰地挑选,有时循环依赖会发生(可能只在糟糕的代码中)。

不幸的是,我还没有找到在构造函数中检测 arguments 类型并自动注入此类实例的方法。

无论如何,我想分享我迄今为止发现的在类中自动注入依赖项的最佳解决方案。 它类似于一个服务定位器,将其服务处理为带有智能指针的 singleton 并可用于依赖注入,但必须对其进行修改以允许具有某些共同依赖关系的两个类获得相同类型的不同实例。

template<typename T>
struct di_explicit
{
    static std::shared_ptr<T> ptr;

    virtual ~di_explicit()
    {
        if(di_explicit<T>::ptr.use_count() == 1) {
            reset();
        }
    }

    virtual std::shared_ptr<T> get()
    {
        return di_explicit<T>::ptr;
    }

    static void reset()
    {
        di_explicit<T>::ptr.reset();
    }

    static void swap(std::shared_ptr<T> arg)
    {
        arg.swap(di_explicit<T>::ptr);
    }

    static void emplace(auto && ... args)
    {
        swap(std::make_shared<T>(std::forward(args) ...));
    }

    static void emplace_if_not_exists(auto && ... args)
    {
        if(!di_explicit<T>::ptr) {
            emplace(std::forward(args) ...);
        }
    }
};

template<typename T>
std::shared_ptr<T> di_explicit<T>::ptr {};

template<typename T>
struct di : di_explicit<T>
{
    di(auto && ... args)
    {
        di_explicit<T>::emplace_if_not_exists(std::forward(args) ...);
    }
};

template<typename T>
struct di_lazy : di_explicit<T>
{
    auto get(auto && ... args)
    {
        di_explicit<T>::emplace_if_not_exists(std::forward(args) ...);
        return di_explicit<T>::ptr;
    }
};

上面代码片段背后的想法是:

它是一个处理另一个 class 的 memory 的逻辑包装器,这样的包装器能够自动创建托管 class 的实例,并在请求时将引用作为 singleton 传递,当不再有对托管的引用时,memory 将自动释放object。

可以使用托管 class(或子类型)的特定实例,以便用户可以声明对所需服务接口的依赖性,并在程序运行时或在测试期间模拟时实例化具体依赖性。

在循环依赖的情况下,有一种方法可以延迟实例化所需的依赖。

基本逻辑编码在基础 class di_explicit<T>中,它使用static shared_ptr<T>来创建单例,以及一个析构函数,当最后一个引用是 static 时重置共享指针(存储在di_explicit<T> ).

struct di: di_explicit<T>在其构造函数中检索依赖关系,而di_lazy: di_explicit<T>仅在请求依赖关系时(在 get() 方法中)执行此操作。

以下是带有模拟的示例(非惰性)。

namespace {
    struct dependency {
        virtual void do_something() {
            std::cout << "doing something" << std::endl;
        }
    };

    struct mock : dependency {
        using dependency::do_something;
        void do_something() {
            std::cout << "mocking something" << std::endl;
        }
    };

    struct srv {
        di<dependency> dep;
        void do_stuff() {
            std::cout << "doing stuff" << std::endl;
            return dep.get()->do_something();
        }
    };

    int test = [](){
        // the classes are not instanciated yet
        std::cout << "ptr exists " << !!di<srv>::ptr << std::endl;
        {
            // the classes instanciated here
            di<srv> s;
            s.get()->do_stuff();
            std::cout << "ptr exists " << !!di<srv>::ptr << std::endl;
        } // <- the instances are destroyed here
        std::cout << "ptr exists " << !!di<srv>::ptr << std::endl;
        
        {
            // use a mock instance
            di_explicit<dependency>::swap(std::make_shared<mock>());
            di<srv>{}.get()->do_stuff();
        } // <- the mock is destroyed here too
        std::cout << "ptr exists " << !!(di<dependency>::ptr) << std::endl;
       
        return 0;
    }();
}

下面是一个带有循环引用和 di_lazy 的例子。

namespace {
    struct dep_2;
    struct dep_3;

    struct dep_1 {
        di_lazy<dep_2> dep;

        void do_something();
    };

    struct dep_2 {
        di_lazy<dep_3> dep;

        void do_something();
    };

    struct dep_3 {
        di_lazy<dep_1> dep;

        void do_something() {
            std::cout << "dep_3 do_something" << std::endl;
            dep.get()->do_something();
        }

        virtual void do_something_else() {
            std::cout << "dep_3 do_something_else" << std::endl;
        }
    };

    void dep_1::do_something() {
        std::cout << "dep_1 do_something" << std::endl;
        dep.get()->do_something();
    }

    void dep_2::do_something() {
        std::cout << "dep_2 do_something" << std::endl;
        dep.get()->do_something_else();
    }

    struct srv_2 {
        di<dep_3> dep;
        void do_something() {
            std::cout << "srv_2 do_something" << std::endl;
            return dep.get()->do_something();
        }
    };

    int result = [](){        
        {
            // neither the dependencies or the service are requested yet
            di_lazy<srv_2> wrapper{};

            // here the service is requested
            auto s = wrapper.get(); 

            // dependencies are requested inside this function
            s->do_something();
        }

        
        {
            struct mock_dep_3 : dep_3 {
                virtual void do_something_else() {
                    std::cout << "dep_3 do_something_else MOCKED!" << std::endl;
                }
            };
            // a mock can be used with di_lazy as well
            di_explicit<dep_3>::swap(std::make_shared<mock_dep_3>());
            di<srv_2>{}.get()->do_something();
        }
        return 0;
    }();
}

我知道还有改进的余地(任何建议都表示赞赏),我希望你觉得它有用

编辑

我找到了一个更好的方法来做同样的事情,但这次扩展了std::shared_ptr class 本身。

它仍然是某种服务定位器,但使用以下代码片段也可以在构造函数中将共享指针作为 arguments 传递

template<typename T>
class di : public std::shared_ptr<T>
{
    static std::shared_ptr<T> ptr;

public:
    static void reset()
    {
        di<T>::ptr.reset();
    }

    static di<T> replace(std::shared_ptr<T> ptr)
    {
        di<T>::ptr = ptr;
        return di<T>::ptr;
    }

    template<typename ... args_t>
    static di<T> emplace(args_t && ... args)
    {
        return di<T>::replace(std::make_shared<T>(
            std::forward<args_t>(args) ...
        ));
    }

    static di<T> instance()
    {
        return di<T>::ptr;
    }

    ~di()
    {
        if(this->is_linked() && di<T>::ptr.use_count() <= 2){
            di<T>::ptr.reset();
        }
    }

    bool is_linked()
    {
        return *this && di<T>::ptr.get() == this->get();
    }

    template<typename ... args_t>
    di(args_t && ... ptr) : std::shared_ptr<T>(std::forward<args_t>(ptr) ...)
    {}
};

template<typename T>
std::shared_ptr<T> di<T>::ptr {};

使用此 class,您可以使用构造函数将某些服务的实例传递给另一个

IE

struct logger_interface
{
    virtual void log(std::string) = 0;
    virtual ~logger_interface() = default;
};

struct some_service_interface
{
    virtual void serve() = 0;
    virtual ~some_service_interface() = default;
};

struct logger_with_id : logger_interface
{
    static int counter;
    int id = ++counter;
    void log(std::string s) {
        std::cout << id << ") " << s << std::endl;
    }
};
int logger_with_id::counter = 0;

struct some_service : some_service_interface
{
    di<logger_interface> logger;

    some_service(
        di<logger_interface> logger = di<logger_interface>::instance()
    ) :
        logger(logger)
    {}

    void serve() {
        logger->log("serving...");
    }
};

int app = []() {
    di<logger_interface>::replace(di<logger_with_id>::emplace());
    di<some_service_interface>::replace(di<some_service>::emplace());
    std::cout << "running app"<< std::endl;
    di<logger_interface>::instance()->log("app");
    di<some_service_interface>::instance()->serve();
    std::cout << std::endl;
    return 0;
}();

将打印

running app
1) app
1) serving...

如果你需要,你可以覆盖某些服务的依赖

struct decorated_logger : logger_interface {
    di<logger_interface> logger;
    decorated_logger(
        di<logger_interface> logger = di<logger_interface>::instance()
    ) :
        logger(logger)
    {}
    void log(std::string s) {
        logger->log("decorating...");
        logger->log(s);
    }
};

int app_with_custom_logger_on_service = [](
    di<logger_interface> logger,
    di<some_service_interface> service
) {
    std::cout << "running app_with_custom_logger_on_service"<< std::endl;
    logger->log("app");
    service->serve();
    std::cout << std::endl;
    return 0;
}(
    di<logger_interface>::replace(std::make_shared<logger_with_id>()),
    di<some_service_interface>::replace(std::make_shared<some_service>(
        std::make_shared<decorated_logger>(std::make_shared<logger_with_id>())
    ))
);

将打印

running app_with_custom_logger_on_service
2) app
3) decorating...
3) serving...

这也可以用于测试

struct mock_logger : logger_interface {
    void log(std::string) {
        std::cout << "mock_logger" << std::endl;
    }
};

struct mock_some_service : some_service_interface {
    void serve() {
        std::cout << "mock_some_service" << std::endl;
    }
};

int test = [](
    di<logger_interface> logger,
    di<some_service_interface> service
) {
    std::cout << "running test"<< std::endl;
    logger->log("app");
    service->serve();
    std::cout << std::endl;
    return 0;
}(
    di<logger_interface>::replace(std::make_shared<mock_logger>()),
    di<some_service_interface>::replace(std::make_shared<mock_some_service>())
);

将打印

running test
mock_logger
mock_some_service

我为这个例子做了一个要点,你可以用 clang 在wandbox上运行它

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM