我的类的构造函数应该执行多少工作?
我有一个代表数据流的类,它基本上 读取或写入文件,但首先对数据进行加密/解密,并且还有一个处理正在访问的媒体的底层编解码器对象。
我正在尝试以 RAII 方式编写这个类,我想要一个干净、漂亮、可用的设计。
令我困扰的是,现在构造函数中正在完成大量工作。 在可以安全地使用对象的 I/O 例程之前,首先需要初始化编解码器(这不是要求很高),然后考虑密钥并初始化加密和其他内容 - 这些需要对以下内容进行一些分析需要大量计算的媒体。
现在我正在构造函数中完成所有这些操作,这使得它需要很长时间。我正在考虑将加密初始化内容(大部分工作)从 ctor 中移出到一个单独的方法中(例如,Stream::auth(key)
),但话又说回来,这会转移一些责任类的用户,因为他们需要在调用任何 I/O 操作之前运行 auth() 。这也意味着我必须在 I/O 调用中进行检查,以验证是否已调用 auth()
。
你认为什么是好的设计?
PS我确实读过类似的问题,但我无法真正将答案应用于此案例。他们大多是“这取决于”... :-/
谢谢
I have a class that represents a data stream, it basically
reads or writes into a file, but first the data are being encrypted/decrypted and there is also an underlying codec object that handles the media being accessed.
I'm trying to write this class in a RAII way and I'd like a clean, nice, usable design.
What bothers me is that right now there is a lot of work being done in the constructor.
Before the object's I/O routines can be safely used, first of all the codec needs to initialized (this isn't very demanding), but then a key is taken into account and crypto and other things are intialized - these require some analysis of the media which takes quite a lot of computation.
Right now I'm doing all this in the constructor, which makes it take a long time. I'm thinking of moving the crypto init stuff (most work) out of the ctor into a separate method (say, Stream::auth(key)
), but then again, this would move some responsibility to the user of the class, as they'd be required to run auth()
before they call any I/O ops. This also means I'd have to place a check in the I/O calls to verify that auth()
had been called.
What do you think is a good design?
P.S. I did read similar question but I wasn't really able to apply the answers on this case. They're mostly like "It depens"... :-/
Thanks
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
唯一真正牢不可破的黄金规则是,在构造函数执行后,类必须处于有效、一致的状态。
您可以选择设计该类,使其在构造函数运行后处于某种“空”/“非活动”状态,也可以将其直接置于其预期的“活动”状态
。 ,最好让构造函数构造您的类。通常,在类真正准备好使用之前,您不会认为它是完全“构造”的,但例外情况确实存在。
然而,请记住,在 RAII 中,关键思想之一是类不应该存在,除非它已经准备好、初始化并且可用。这就是为什么它的析构函数会进行清理,这就是为什么它的构造函数应该进行设置。
同样,例外确实存在(例如,某些 RAII 对象允许您释放资源并尽早执行清理,然后让析构函数不执行任何操作。)
因此,归根结底,这取决于您自己的判断。
从不变量的角度来思考它。如果我获得了你们班级的一个实例,我可以依赖什么?我对它的假设越多,它就越容易使用。如果它可能可以使用,并且可能处于某种“已构造,但未初始化”状态,并且可能处于“已清理”状态已启动但未销毁”状态,那么使用它很快就会变得痛苦。
另一方面,如果它保证“如果该对象存在,则可以按原样使用它”,那么我就会知道我可以使用它,而不必担心之前对它做了什么。
听起来你的问题是你在构造函数中做了太多事情。
如果您将作业分成多个较小的班级怎么办?让编解码器单独初始化,然后我可以简单地将已经初始化的编解码器传递给您的构造函数。所有的身份验证和加密内容以及诸如此类的东西也可以移出到单独的对象中,然后在准备好后简单地传递给“this”构造函数。
然后剩下的构造函数不必从头开始做所有事情,而是可以从一些已经初始化并准备好使用的辅助对象开始,所以它只需要连接点即可。
The only truly golden unbreakable rule is that the class must be in a valid, consistent, state after the constructor has executed.
You can choose to design the class so that it is in some kind of "empty"/"inactive" state after the constructor has run, or you can put it directly into the "active" state that it is intended to be in.
Generally, it should be preferred to have the constructor construct your class. Usually, you wouldn't consider a class fully "constructed", until it's actually ready to be used, but exceptions do exist.
However, keep in mind that in RAII, one of the key ideas is that the class shouldn't exist unless it is ready, initalized and usable. That's why its destructor does the cleanup, and that's why its constructor should do the setup.
Again, exceptions do exist (for example, some RAII objects allow you to release the resource and perform cleanup early, and then have the destructor do nothing.)
So at the end of the day, it depends, and you'll have to use your own judgment.
Think of it in terms of invariants. What can I rely on if I'm given an instance of your class? The more I can safely assume about it, the easier it is to use. If it might be ready to use, and might be in some "constructed, but not initialized" state, and might be in a "cleaned up but not destroyed" state, then using it quickly becomes painful.
On the other hand, if it guarantees that "if the object exists, it can be used as-is", then I'll know that I can use it without worrying about what was done to it before.
It sounds like your problem is that you're doing too much in the constructor.
What if you split the work up into multiple smaller classes? Have the codec be initialized separately, then I can simply pass the already-initialized codec to your constructor. And all the authentication and cryptography stuff and whatnot could possibly be moved out into separate objects as well, and then simply passed to "this" constructor once they're ready.
Then the remaining constructor doesn't have to do everything from scratch, but can start from a handful of helper objects which are already initialized and ready to be used, so it just has to connect the dots.
您可以将检查放在 IO 调用中以查看是否已调用 auth,如果已调用,则继续,如果没有,则调用它。
这减轻了用户的负担,并将费用推迟到需要时。
you could just place the check in the IO calls to see if auth has been called, and if it has, then continue, if not, then call it.
this removes the burden from the user, and delays the expense until needed.
基本上,这一切都归结为从以下三种设计中选择哪种设计:
设计
免责声明:本文并不鼓励使用异常规范或异常。如果您愿意,也可以使用错误代码来报告错误。此处使用的异常规范只是为了使用简洁的语法来说明何时会发生不同的错误。
设计 1
这是最常见的设计,并且完全非 RAII。构造函数只是将对象置于某种陈旧状态,并且在构造发生后必须手动初始化每个实例。
优点:
CoCreateObject()
不允许您向对象构造函数转发额外的参数)。有时,仍然有解决方法,例如 builder 对象。缺点:
std::ostream
)。设计2
这是RAII 方法。确保对象 100% 可用,没有额外的人工制品(例如,在每个实例上手动调用
stream.initialize(...);
。优点:
缺点:
设计 3
在前两种情况之间有所折衷,但不要初始化,但让其他方法延迟调用内部
。必要时使用 initialize(...)
方法优点:
:
讨论
如果您绝对必须为每个实例的初始化付费,那么设计 #1 就不可能了,因为它只会导致软件中出现更多错误。
问题只是关于何时支付初始化成本。您喜欢预先付款还是首次使用时付款?在大多数情况下,我更喜欢预先付费,因为我不想假设用户可以稍后在程序中处理错误。但是,您的程序中可能存在特定的线程语义,并且您可能无法在创建时(或者相反,在使用时)停止线程。
无论如何,您仍然可以通过在设计 #2 中使用类的动态分配来获得设计 #3 的好处。
结论
基本上,如果您犹豫的唯一原因是构造函数快速执行的某种哲学理想,那么我会选择纯 RAII 设计。
Basically, this all boils down to which design to choose from the following three:
Designs
Disclaimer: this post is not encouraging the use of exception specifications or exceptions for that matter. The errors may equivalently be reported using error codes if you wish. Exception specifications as used here are just meant to illustrate when different errors can occur using a concise syntax.
Design 1
This is the most recurring design out there, and totally non-RAII. The constructor just puts the object in some stale state and each instance must be initialized manually after construction takes place.
Pros:
CoCreateObject()
does not allow you to forward extra arguments the object constructor). Sometimes, there are still workarounds, such as a builder object.Cons:
std::ostream
).Design 2
This is the RAII approch. Make sure the object is 100% usable with no extra artefacts (e.g. manually calling
stream.initialize(...);
on each instance.Pros:
Cons:
Design 3
Somewhat of a compromise between the two previous cases. Don't initialize yet, but have the other methods lazily invoke the internal
.initialize(...)
method when necessary.Pros:
Cons:
Discussion
If you absolutely must pay for the initialization for every instance, then design #1 is out of the question as it just results in more bugs in the software.
The question is just about when to pay for the initialization cost. Do you prefer paying it upfront, or on first use? In most scenarios, I prefer paying upfront because I don't want to assume users can handle errors later in the program. However, there might be specific threading semantics in your program, and you might not be able to stall threads at creation time (or, conversely, at use time).
In any case, you can still get the benefits of design #3 by using dynamic allocation of the class in design #2.
Conclusion
Basically, if the only reason you are hesitating is for some philosophical ideal where constructors execute quickly, I would just go with the pure RAII design.
对此没有硬性规定,但一般来说,最好避免使用繁重的构造函数,原因有两个(也许还有其他原因):
There's no hard and fast rule on this, but in general it's best to avoid heavy constructors for two reasons that come to mind (maybe others as well):