I am having a hard time figuring how to design classes that can’t initialize all their internal members in the constructor. I know that this should be something basic and discussed all over the net, but I’m not sure what to look for. So, for example, please consider the following code:
#include <iostream>
class Workhorse
{
public:
void SetData (const int &data)
{
this->data = data;
}
int GetData () const
{
return this->data;
}
private:
int data;
};
class Worker
{
public:
Worker ()
{
}
void Initialize (const int &data)
{
horse.SetData(data);
}
void Action () const
{
std::cout << horse.GetData() << std::endl;
}
private:
Workhorse horse;
};
int main ()
{
Worker worker;
worker.Initialize(3);
worker.Action();
return 0;
}
I want to prevent the workers from calling any methods without first calling Initialize(). The layman’s implementation would be to add an isInitialized flag in the Worker class, set it to true in Initialize() and test it at the beginning of each public method (maybe also in the protected / private ones, if we introduce some inheritance?). Unfortunately, this seems a bit cumbersome and hard to maintain. Also, it’s just awful to repeat an if statement in all methods. I haven’t even began to ponder about thread safety issues, but, right now, I’m only implementing a single-threaded application. Is there a smarter way to design this?
EDIT: OK, I chose a dumb design as an example, which, indeed, is flawed. Let me try to give a clearer picture of what I have:
#include <iostream>
class PublicKeyCryptoProvider
{
public:
struct PublicKey
{
int shared;
};
struct PrivateKey
{
int secret;
};
int Encrypt (const int &plaintext) const
{
int ciphertext;
//apply encryption algorithm on plaintext
ciphertext = plaintext * this->pk.shared;
return ciphertext;
}
int Decrypt (const int &ciphertext) const
{
int plaintext;
//apply decryption algorithm on ciphertext
plaintext = ciphertext / this->sk.secret;
return plaintext;
}
void GenerateKeys ()
{
this->pk.shared = 4;
this->sk.secret = 4;
//generate pk and sk
}
void SetPublicKey (const PublicKey &pk)
{
this->pk = pk;
}
const PublicKey &GetPublicKey () const
{
return this->pk;
}
private:
PublicKey pk;
PrivateKey sk;
};
int main ()
{
/* scenario 1: */
PublicKeyCryptoProvider cryptoProvider;
cryptoProvider.GenerateKeys();
std::cout << cryptoProvider.Decrypt(cryptoProvider.Encrypt(3)) << std::endl;
/* /scenario 1: */
/* scenario 2: */
PublicKeyCryptoProvider cryptoProvider1;
cryptoProvider1.GenerateKeys();
PublicKeyCryptoProvider cryptoProvider2;
cryptoProvider2.SetPublicKey(cryptoProvider1.GetPublicKey());
int ciphertext = cryptoProvider2.Encrypt(3);
std::cout << cryptoProvider1.Decrypt(ciphertext) << std::endl;
//now let's do something bad...
std::cout << cryptoProvider2.Decrypt(ciphertext) << std::endl;
/* /scenario 2: */
return 0;
}
Obviously, you can imagine real life examples where scenario 2 is perfectly valid. Given the above situation, is there any better option than adding a canDecrypt flag inside the PublicKeyCryptoProvider class, which is set to true when generating keys and then tested at the beginning of the decrypt method? I have to mention that this is a very simple example, because, in my case, the PublicKeyCryptoProvider can perform faster encryptions if it is the owner of the secret key and it has much more public methods, so I would be doomed to test the flag more than a couple of times… Also, I have a client – server mockup scenario where the server exposes a bunch of public methods for the client, but the client can only call the methods after it has called the Initialize() method on the server…
Great question! Its always good to make an API that is hard to use wrong, and as you are observing classes that are not fully constructed are dangerous, hard to use correctly and easy to use wrong. They set ourselves & others up for failure. I’ve done some refactoring on your second example to come up with a safer design that won’t even allow your “do something bad” code.
The general idea was that
PublicKeyCryptoProviderhad too many responsibilities ( violation of SRP ):Each one of the responsibilities has been delegated out. Now the
PublicKeyCryptoProvideris more responsible for giving you the tools necessary to do encryption/decryption & key management.