使用引用作为依赖项的类成员

pht*_*ier 24 c++ dependency-injection

我在内存管理语言上花了一些时间后回到C++,而且我突然想知道实现依赖注入的最佳方法是什么.(我完全卖给DI,因为我发现它是使测试驱动设计变得非常简单的最简单方法).

现在,浏览SO和谷歌在这件事上得到了很多意见,我有点困惑.

作为这个问题的答案,C++中的依赖注入,有人建议你不要传递原始指针,即使是依赖注入.据我所知,这与对象的所有权有关.

现在,在臭名昭着的谷歌风格指南中,也解决了对象的所有权问题(虽然没有详细说明我的州;)):http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Smart_Pointers

所以我理解的是,为了使哪个对象拥有其他对象的所有权更清楚,你应该避免传递原始指针.特别是,它似乎反对这种编码:

class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   Dependency * dependency_;
public:
   Addict(Dependency * dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_->do_something();
   }
   // ... whatever ... 
};    
Run Code Online (Sandbox Code Playgroud)

如果Dependency是一个纯虚拟类(又名穷人 - 接口),那么这个代码可以很容易地注入依赖的模拟版本(使用像google mock这样的东西).

问题是,我真的没有看到这种代码可以解决的问题,以及为什么我应该使用除了原始指针以外的任何东西!是不是很清楚依赖来自哪里?

另外,我读了很多帖子,暗示在这种情况下应该使用引用,这样的代码更好吗?

class Addict {
   // Something I depend on (hence, the Addict name. sorry.)
   const Dependency & dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};
Run Code Online (Sandbox Code Playgroud)

但后来我得到了其他同样权威的建议,反对使用引用作为成员:http://billharlan.com/pub/papers/Managing_Cpp_Objects.html

正如你所看到的,我不确定各种方法的相对优缺点,所以我有点困惑.我很抱歉,如果这已经被讨论过,或者只是在给定项目中只是个人选择和一致性的问题......但任何想法都是受欢迎的.

谢谢

PH


答案摘要

(我不知道这样做是不是很好,但是我会从答案中得到的代码示例...)

从各种回复中,我可能最终会在我的案例中做到:

  • 传递依赖关系作为参考(至少要确保不可能为NULL)
  • 在无法进行复制的一般情况下,明确禁止复制,并将依赖关系存储为参考
  • 在可以进行复制的罕见情况下,将依赖关系存储为RAW指针
  • 让依赖项的创建者(某种工厂)决定动态分配的堆栈分配(在这种情况下,通过智能指针进行管理)
  • 建立一个约定来将依赖关系与自己的资源分开

所以我最终会得到类似的东西:

class NonCopyableAddict {
    Dependency & dep_dependency_;

    // Prevent copying
    NonCopyableAddict & operator = (const NonCopyableAddict & other) {}
    NonCopyableAddict(const NonCopyableAddict & other) {}

public:
    NonCopyableAddict(Dependency & dependency) : dep_dependency_(dep_dependency) {
    }
    ~NonCopyableAddict() {
      // No risk to try and delete the reference to dep_dependency_ ;)
    }
    //...
    void do_some_stuff() {
      dep_dependency_.some_function();
    }
};
Run Code Online (Sandbox Code Playgroud)

对于可复制的课程:

class CopyableAddict {
    Dependency * dep_dependency_;

public: 
    // Prevent copying
    CopyableAddict & operator = (const CopyableAddict & other) {
       // Do whatever makes sense ... or let the default operator work ? 
    }
    CopyableAddict(const CopyableAddict & other) {
       // Do whatever makes sense ...
    }


    CopyableAddict(Dependency & dependency) : dep_dependency_(&dep_dependency) {
    }
    ~CopyableAddict() {
      // You might be tempted to delete the pointer, but its name starts with dep_, 
      // so by convention you know it is not your job
    }
    //...
    void do_some_stuff() {
      dep_dependency_->some_function();
    }
};
Run Code Online (Sandbox Code Playgroud)

根据我的理解,没有办法表达"我有一个指向某些东西的指针,但我不拥有它",编译器可以强制执行.所以我不得不在这里诉诸命名约定......


保持参考

正如Martin指出的那样,以下示例并未解决问题.

或者,假设我有一个复制构造函数,例如:

class Addict {
   Dependency dependency_;
  public:
   Addict(const Dependency & dependency) : dependency_(dependency) {
   }
   ~Addict() {
     // Do NOT release dependency_, since it was injected and you don't own it !
   }
   void some_method() {
     dependency_.do_something();
   }
   // ... whatever ... 
};
Run Code Online (Sandbox Code Playgroud)

Mar*_*ork 8

没有硬性和快速的规则:
正如人们提到的,在对象中使用引用会导致复制问题(并且确实如此)因此它不是灵丹妙药,但对于某些情况它可能是有用的(这就是为什么C++给我们选择这些不同的方式做到这一点).但是使用RAW指针实际上不是一个选择.如果您是动态分配对象,那么您应该始终使用智能指针维护它们,并且您的对象也应该使用智能指针.

对于需要示例的人:Streams总是作为引用传递和存储(因为它们无法复制).

关于您的代码示例的一些评论:

例一和二

你的第一个带指针的例子.与使用引用的第二个示例基本相同.不同之处在于引用不能为NULL.当您传递引用时,该对象已经存活,因此其寿命应该大于您正在测试的对象(如果它是在堆栈上创建的),那么保留引用应该是安全的.如果您动态创建指针作为依赖项,我会考虑使用boost :: shared_pointer或std :: auto_ptr,具体取决于是否共享依赖项的所有权.

例三:

我认为你的第三个例子并没有什么用处.这是因为您不能使用多态类型(如果您传递从依赖项派生的对象,它将在复制操作期间被切片).因此代码也可以在Addict中,而不是单独的类.

比尔哈伦:(http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html)

不要拿走Bill But的任何东西:

  1. 我从未听说过他.
    • 他是地理医师,不是计算机程序员
    • 他建议用Java编程来改进你的C++
    • 现在语言的使用方式如此不同,这是完全错误的.
    • 如果您想使用什么做什么/不做什么的参考.
      然后我会选择C++领域的一个大牌:
      Stroustrup/Sutter/Alexandrescu/Meyers

摘要:

  1. 不要使用RAW指针(需要所有权时)
  2. 使用智能指针.
  3. 不要将对象复制到对象中(它会切片).
  4. 您可以使用引用(但知道限制).

我使用引用的依赖注入示例:

class Lexer
{
    public: Lexer(std::istream& input,std::ostream& errors);
    ... STUFF
    private:
       std::istream&  m_input;
       std::ostream&  m_errors;
};
class Parser
{
    public: Parser(Lexer& lexer);
    ..... STUFF
    private:
        Lexer&        m_lexer;
};

int main()
{
     CLexer  lexer(std::cin,std::cout);  // CLexer derived from Lexer
     CParser parser(lexer);              // CParser derived from Parser

     parser.parse();
}

// In test.cpp
int main()
{
     std::stringstream  testData("XXXXXX");
     std::stringstream  output;
     XLexer  lexer(testData,output);
     XParser parser(lexer);

     parser.parse();
}
Run Code Online (Sandbox Code Playgroud)


Joe*_*oeG 5

总结:如果需要存储引用,请将指针存储为私有变量,并通过取消引用它的方法访问它。您可以检查对象的不变量中的指针是否为空。

深入:

首先,在类中存储引用使得无法实现合理合法的复制构造函数或赋值运算符,因此应该避免它们。使用一个通常是错误的。

其次,传递给函数和构造函数的指针/引用的类型应该表明谁负责释放对象以及应该如何释放它:

  • std::auto_ptr - 被调用的函数负责释放,完成后会自动释放。如果你需要复制语义,接口必须提供一个克隆方法,它应该返回一个 auto_ptr。

  • std::shared_ptr - 被调用的函数负责释放,并在它完成时以及对对象的所有其他引用都消失时自动释放。如果您需要浅拷贝语义,编译器生成的函数就可以了,如果您需要深拷贝,接口必须提供一个克隆方法,该方法应该返回一个 shared_ptr。

  • 参考 - 调用者有责任。你不在乎 - 对象可能是你知道的堆栈分配的。在这种情况下,您应该通过引用传递但通过指针存储。如果你需要浅拷贝语义,编译器生成的函数就可以了,如果你需要深拷贝,你就有麻烦了。

  • 一个原始指针。谁知道?可以分配到任何地方。可能为空。您可能负责释放它,也可能不负责。

  • 任何其他智能指针 - 它应该为您管理生命周期,但您需要查看文档以了解复制的要求。

请注意,让您负责释放对象的方法不会破坏 DI - 释放对象只是您与接口签订的合同的一部分(因为您不需要了解有关具体类型的任何信息来释放它)。

  • 为什么原始指针不知道谁应该释放它?如果您没有创建它,那么您不应该释放它。这有什么问题吗? (2认同)