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

时间:2009-12-29 13:39:18

标签: c++ dependency-injection

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

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

作为这个问题的答案,Dependency injection in 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 ... 
};    

如果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 ... 
};

然后我使用引用作为成员获得反对的其他同样权威的建议: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 so_some_stuff() {
      dep_dependency_.some_function();
    }
};

对于可复制的课程:

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 you job
    }
    //...
    void so_some_stuff() {
      dep_dependency_->some_function();
    }
};

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


保留供参考

正如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 ... 
};

7 个答案:

答案 0 :(得分: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的/萨特/ Alexandrescu的/迈尔斯
  2. 要点:

    1. 不要使用RAW指针(需要所有权时)
    2. 请使用智能指针。
    3. 不要将对象复制到对象中(它会切片)。
    4. 您可以使用引用(但知道限制)。
    5. 我使用引用的依赖注入示例:

      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();
      }
      

答案 1 :(得分:4)

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

深入:

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

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

  • std :: auto_ptr - 被调用函数具有可重用性,并在完成后自动执行。如果需要复制语义,接口必须提供一个应该返回auto_ptr的克隆方法。

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

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

  • 原始指针。谁知道?可以在任何地方分配。可以为null。你可能有可释放它的可行性,你可能没有。

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

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

答案 2 :(得分:1)

[更新1]
如果您始终可以保证依赖性超过了上瘾者,那么可以使用原始指针/引用。在这两者之间,我做了一个非常简单的决定:指针是否允许NULL,否则引用。

(我原来的帖子的一点是指针和参考都没有解决生命周期问题)


我会遵循臭名昭着的谷歌风格指南,并使用智能指针。

指针和引用都有同样的问题:你需要确保依赖性超过了上瘾者。这将一个非常讨厌的责任推到了客户身上。

使用(引用计数的)智能指针,当没有人再使用它时,策略变为依赖性被破坏。对我来说听起来很完美。

更好:使用boost::shared_ptr(或类似的智能指针允许类型中立的销毁策略)策略附加到构造中的对象 - 这通常意味着影响依赖最终会在一个地方出现。

智能指针的典型问题 - 开销和循环引用 - 在这里很少发挥作用。依赖实例通常不是很小而且数量众多,并且对其成瘾者有强烈参考的依赖性至少是代码味道。 (仍然,你需要记住这些事情。欢迎回到C ++)

警告:我没有“完全卖掉”给DI,但我完全卖掉了智能指针;)

[更新2]
请注意,您始终可以使用空删除器为堆栈/全局实例创建shared_ptr 这需要双方都支持这一点,但是:addict必须保证它不会将对依赖项的引用转移给可能活得更久的其他人,并且调用者回来负责确保生命周期。我不满意这个解决方案,但偶尔也会使用它。

答案 3 :(得分:1)

我会避免引用作为成员,因为如果最终将一个对象粘贴在STL容器中,它们往往不会导致头痛。我会考虑将boost::shared_ptr用于所有权,将boost::weak_ptr用于家属。

答案 4 :(得分:0)

之前有人问过,但是我的SO搜索技能还没有找到它。总结一下我的立场 - 你应该很少(如果有的话)使用引用作为类成员。这样做会导致各种初始化,分配和复制问题。而是使用指针或值。

编辑:找到一个 - 这是一个有各种意见的问题作为答案:Should I prefer pointers or references in member data?

答案 5 :(得分:0)

  

但是我得到了其他同样权威的建议,反对使用引用作为成员:http://billharlan.com/pub/papers/Managing%5FCpp%5FObjects.html

在这种情况下,我认为你只想在构造函数中设置一次对象,并且永远不要改变它,所以没问题。 但是如果你想稍后改变它,使用init函数,有一个复制构造函数,简而言之,所有必须更改引用的东西都必须使用指针。

答案 6 :(得分:0)

我可以在这里进行我的降级调整,但是我会说在课堂上不应该有任何参考成员。除非它们是一个简单的常数值。造成这种情况的原因很多,第二个是你开始这个,你用C ++打开了所有不好的东西。如果你真的在乎,请查看我的博客。