How much work should constructor of my class perform?

允我心安 提交于 2020-01-03 13:09:40

问题


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


回答1:


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.




回答2:


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.




回答3:


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.

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

Pros:

  1. Users have control over when to invoke the "heavy" initialization process
  2. The object can be created before the key exists. This is important for frameworks such as COM, where all objects must have a default constructor (the CoCreateObject() does not allow you to forward extra arguments the object constructor). Sometimes, there are still workarounds, such as a builder object.

Cons:

  1. Objects must be checked for the stale state before using the object. This may be enforced by the object by returning an error code or throwing an exception. Personally, I hate objects that allow me to use them and just appear to ignore my calls (e.g. a failed 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.

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

Pros:

  1. The object can always be assumed to be in a valid state. This is so much simpler to use.

Cons:

  1. Constructor might take a long time to execute.
  2. All required arguments must be available at the instance construction. This has once in a while been a problem for me, especially if most other objects in the code base use design #1.

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.

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

Pros:

  1. Almost as easy to use as design #1. Almost (see below).

Cons:

  1. If the initialization step may fail, it may now fail anywhere there is a first call to any of the public methods. Proper error handling for this scenario is extremely difficult.

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.




回答4:


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):

  • The order of the objects created intializer list can give rise to subtle bugs
  • What to do with exceptions in the constructor? Will you need to handle partially-constructed objects in your app?


来源:https://stackoverflow.com/questions/8915873/how-much-work-should-constructor-of-my-class-perform

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!