我班级的建设者应该做多少工作?

时间:2012-01-18 19:10:43

标签: c++ raii

我有一个代表数据流的类,基本上 读取或写入文件,但首先是数据被加密/解密,还有一个底层编解码器对象来处理被访问的媒体。

我正在尝试以RAII的方式编写这个类,我想要一个干净,漂亮,可用的设计。

困扰我的是,现在构造函数中正在做很多工作。 在可以安全地使用对象的I / O例程之前,首先需要初始化编解码器(这不是很苛刻),但是然后考虑密钥并且加密和其他内容被初始化 - 这些需要一些分析需要大量计算的媒体。

现在我在构造函数中执行所有这些操作,这需要很长时间。我正在考虑将加密初始化的东西(大多数工作)从ctor转移到一个单独的方法(比如Stream::auth(key)),但是再次,这会将一些责任转移给类的用户,因为它们在调用任何I / O操作之前,需要运行auth() 。这也意味着我必须检查I / O调用以验证是否已调用auth()

您认为什么是好设计?

P.S。我确实读过类似的问题,但我真的无法在这个案例中应用答案。他们大多喜欢“它取决于......”: - /

由于

4 个答案:

答案 0 :(得分:7)

唯一真正的黄金牢不可破的规则是在构造函数执行完之后,类必须处于有效,一致的状态。

您可以选择设计类,使其在构造函​​数运行后处于某种“空”/“非活动”状态,或者您可以将其直接置于“活动”状态,即它应该是英寸

通常,最好让构造函数构造您的类。通常,你不会认为一个完全“构造”的类,直到它实际上准备好被使用,但是存在异常。 但是,请记住,在RAII中,关键思想之一是该类不应该存在,除非它已准备好,初始化和可用。这就是它的析构函数进行清理的原因,这就是它的构造函数应该进行设置的原因。

同样,存在异常(例如,某些RAII对象允许您尽早释放资源并执行清理,然后让析构函数不执行任何操作。) 所以在一天结束时,这取决于你,你必须使用自己的判断。

用不变量来思考它。如果我有一个班级实例,我还能依靠什么?我越可以安全地假设它越容易使用。如果可能准备好使用,并且可能处于某种“构造但未初始化”状态,并且可能处于“已清理”状态起来但没有被破坏“状态,然后迅速使用它变得痛苦。

另一方面,如果它保证“如果对象存在,它可以按原样使用”,那么我就知道我可以使用它而不用担心之前做了什么。

听起来你的问题是你在构造函数中做得太多了。

如果将工作分成多个较小的类,该怎么办?让编解码器单独初始化,然后我可以简单地将已经初始化的编解码器传递给构造函数。并且所有身份验证和加密的东西以及诸如此类的东西都可以移动到单独的对象中,然后在它们准备就绪后简单地传递给“this”构造函数。

然后剩下的构造函数不必从头做任何事情,但可以从一些已经初始化并准备好使用的辅助对象开始,所以它只需连接点。

答案 1 :(得分:2)

您可以将检查放入IO调用以查看是否已调用auth,如果有,则继续,如果没有,则调用它。

这消除了用户的负担,并将费用延迟到需要之前。

答案 2 :(得分:2)

基本上,这一切都归结为从以下三个中选择哪种设计:

设计

免责声明:这篇文章不鼓励使用例外规范或例外。如果您愿意,可以使用错误代码等效地报告错误。此处使用的异常规范仅用于说明何时可以使用简洁的语法发生不同的错误。


设计1

这是最常见的设计,完全不是RAII。构造函数只是将对象置于某种陈旧状态,并且每个实例必须在构造完成后手动初始化。

class SecureStream
{
public:
    SecureStream();
    void initialize(Stream&,const Key&) throw(InvalidKey,AlreadyInitialized);
    std::size_t get(      void*,std::size_t) throw(NotInitialized,IOError);
    std::size_t put(const void*,std::size_t) throw(NotInitialized,IOError);
};

<强>赞成

  1. 用户可以控制何时调用“繁重的”初始化过程
  2. 可以在密钥存在之前创建对象。这对于诸如COM之类的框架很重要,其中所有对象必须具有默认构造函数(CoCreateObject()不允许您转发对象构造函数的额外参数)。有时,仍有解决方法,例如builder对象。
  3. <强>缺点

    1. 在使用对象之前,必须检查对象的陈旧状态。通过返回错误代码或抛出异常,对象可以强制执行此 。就个人而言,我讨厌允许我使用它们的对象,并且似乎忽略了我的呼叫(例如失败的std::ostream)。

    2. 设计2

      这是RAII approch。确保对象100%可用,没有额外的假象(例如,在每个实例上手动调用stream.initialize(...);

      class SecureStream
      {
      public:
          SecureStream(Stream&,const Key&) throw(InvalidKey);
          std::size_t get(      void*,std::size_t) throw(IOError);
          std::size_t put(const void*,std::size_t) throw(IOError);
      };
      

      <强>赞成

      1. 该对象可以始终被认为处于有效状态。这样使用起来非常简单。
      2. <强>缺点

        1. 构造函数可能需要很长时间才能执行。
        2. 所有必需的参数必须在实例构造中可用。这对我来说曾经是一个问题,特别是如果代码库中的大多数其他对象使用设计#1。

        3. 设计3

          前两个案例之间有些妥协。不要初始化,但必要时让其他方法懒惰地调用内部.initialize(...)方法。

          class SecureStream
          {
          public:
              SecureStream(Stream&,const Key&);
              std::size_t get(      void*,std::size_t) throw(InvalidKey,IOError);
              std::size_t put(const void*,std::size_t) throw(InvalidKey,IOError);
          private:
              void initialize() throw(InvalidKey);
          };
          

          <强>赞成

          1. 几乎和设计#1一样易于使用。 几乎(见下文)。
          2. <强>缺点

            1. 如果初始化步骤可能失败,则现在可能会在第一次调用任何公共方法的任何地方失败。对这种情况进行适当的错误处理非常困难。

            2. 讨论

              如果你绝对必须支付每个实例的初始化费用,那么设计#1是不可能的,因为它只会导致软件中出现更多错误。

              问题只是何时支付初始化费用。您更喜欢先付款还是先付款?在大多数情况下,我更喜欢先付款,因为我不想假设用户可以在程序中稍后处理错误。但是,程序中可能存在特定的线程语义,并且您可能无法在创建时(或者相反地,在使用时)停止线程。

              在任何情况下,您仍然可以通过在设计#2中使用动态分配类来获得设计#3的好处。

              结论

              基本上,如果你犹豫的唯一原因是构造者快速执行的哲学理想,我会选择纯粹的RAII设计。

答案 3 :(得分:-1)

对此没有严格的规则,但总的来说,最好避免使用重型构造函数,因为有两个原因(也许是其他原因):

  • 创建的intializer列表的顺序可能会产生微妙的错误
  • 如何处理构造函数中的异常?您是否需要在应用程序中处理部分构造的对象?