设计延迟加载

时间:2012-06-27 10:20:05

标签: c++ design-patterns

我很难想象如何设计无法在构造函数中初始化所有内部成员的类。我知道这应该是基本的,并在网上讨论,但我不知道该寻找什么。因此,例如,请考虑以下代码:

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

我希望在没有首先调用Initialize()的情况下阻止worker调用任何方法。外行人的实现是在Worker类中添加一个isInitialized标志,在Initialize()中将其设置为true并在每个公共方法的开头测试它(如果我们引入一些继承,也可能在受保护/私有方法中测试?) 。不幸的是,这看起来有点麻烦且难以维护。而且,在所有方法中重复if语句真的很糟糕。我甚至没有开始思考线程安全问题,但是,现在,我只是实现了单线程应用程序。有没有更聪明的方法来设计它?


编辑:好的,我选择了一个愚蠢的设计作为一个例子,实际上是有缺陷的。让我试着更清楚地了解我的情况:

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

显然,您可以想象场景2完全有效的现实生活示例。鉴于上述情况,有没有比在PublicKeyCryptoProvider类中添加canDecrypt标志更好的选择,在生成密钥时将其设置为true,然后在解密方法的开头进行测试?我必须提到这是一个非常简单的例子,因为在我的情况下,如果PublicKeyCryptoProvider是密钥的所有者并且它有更多的公共方法,它可以执行更快的加密,所以我注定要测试标志多次...而且,我有一个客户端 - 服务器模型场景,其中服务器为客户端公开了一堆公共方法,但客户端只能在调用Initialize()方法之后调用这些方法服务器......

5 个答案:

答案 0 :(得分:2)

我会做以下事情:

    class Worker
{
public:
    Worker (const int& data)
    {
        horse.SetData(data);
    }

    void Action () const
    {
        std::cout << horse.GetData() << std::endl;
    }

private:
    Workhorse horse;
};

由于你显然不希望一个Worker对象在没有被初始化的情况下存在,它的初始化应该是它构造的一部分,它应该在没有这个初始化的情况下进行实例化,因为没有它就无法工作。

答案 1 :(得分:2)

听起来你感兴趣的行为需要有一个充当经理的类,决定是否提供对Workhorse功能之一的访问,或者是虚拟功能。一种可能性是创建一个抽象父类(Horse),指定Workhorse的接口,但不实现任何功能。从中派生出两个类,Workhorse和TrojanHorse。 TrojanHorse会实现父类中的所有函数,因为Shells,Workhorse就像你已经创建它一样。

经理类可以拥有你感兴趣的初始化函数,它可以存储一个类型为Horse的对象。默认情况下,可以将horse对象分配给TrojanHorse对象,但初始化会将其分配给Workhorse对象。

这个解决方案几乎可以避免来自if语句的所有速度影响,如果类没有以正确的方式改变,编译器会给出错误,它将是可维护的,并且它仍然可以理解为另一个程序员看代码。

答案 2 :(得分:1)

你提到你并不认为继承是可行的方法,但是有一种相当简洁的方法可以用最小的继承来做这件事。

这里有一些设计模式很有用。如果将接口从实现中分离出来并将实现视为“总是返回错误”并“执行一些有用的操作”,则可以将这两个实现视为策略,将接口视为代理。

代理总是转发它对实现的调用,并且总是有一个实现(不需要检查标志)。

使用默认实现初始化接口,该实现会导致某种错误(断言,抛出等)。这是一个例子

这是我用Clang 3.2编译的一个例子:

#include <iostream>
#include <memory>
#include <cassert>
#include <stdexcept>

// Base class that defines the signatures of the functions to be forwarded.
// Another nice benefit is that each implementation can store whatever 
// specific data they need.
class Impl {
public:
    virtual void FuncA() = 0;
};


typedef std::unique_ptr<Impl> ImplPtr;


class ErrorImpl : public Impl {
public:
    virtual void FuncA() { 
        assert(!"Don't call this before calling InitializeImpl!");
        throw std::runtime_error("Don't call this before calling InitializeImpl!");  
    }     
};

class OtherImpl : public Impl {
public:
    void FuncA() { 
        std::cout << "Some other useful functionality here.\n";
    }         
};

// This is the class that user's will call.
class Proxy {
public:
    Proxy() : impl_( ImplPtr(new ErrorImpl) ) {}

    void InitializeImpl( ImplPtr ptr ) {
        // You must std::move std::unique_ptr's.
        impl_ = std::move( ptr );
    }
    void FuncA() { impl_->FuncA(); }

private:
    ImplPtr impl_;
};


int main( int, char**) {
    Proxy p;
    // p.FuncA(); // asserts & throws.

    p.InitializeImpl( ImplPtr(new OtherImpl) );
    p.FuncA();

    return 0;
}

答案 3 :(得分:1)

好问题!它总是很好地制作一个难以使用的API错误,并且因为您正在观察未完全构造的类是危险的,难以正确使用且易于使用错误。他们为自己设定了其他人失败了。我已经对你的第二个例子做了一些重构,想出一个更安全的设计,甚至不允许你的“做坏事”代码。

一般的想法是PublicKeyCryptoProvider承担了太多责任(违反SRP):

  • 密钥生成
  • 密钥存储
  • 加密
  • 解密

每项责任都已下放。现在PublicKeyCryptoProvider更负责为您提供加密/解密所需的工具。密钥管理。

#include <iostream>
#include <utility>

struct PublicKey
{
    int shared;
};
struct PrivateKey
{
    int secret;
};

struct KeyPair
{
    PublicKey public_key;
    PrivateKey private_key;
};


struct Encryptor
{
    Encryptor( PublicKey shared_ )
     : shared( shared_ )
    {}

    int Encrypt (const int &plaintext) const
    {
        int ciphertext;
        //apply encryption algorithm on plaintext
        ciphertext = plaintext * shared.shared;
        return ciphertext;
    }

private:
    PublicKey shared;
};

struct Decryptor
{
    Decryptor( PrivateKey secret_ )
     : secret( secret_ )
    {}

    int Decrypt (const int &ciphertext) const
    {
        int plaintext;
        //apply decryption algorithm on ciphertext
        plaintext = ciphertext / secret.secret;

        return plaintext;
    }

private:
    PrivateKey secret;
};

class PublicKeyCryptoProvider
{
public:

    KeyPair GenerateKeys()
    {
        KeyPair keys;

        //generate pk and sk
        keys.public_key.shared = 4;
        keys.private_key.secret = 4;

        return keys;
    }


    Decryptor BuildDecryptor( PrivateKey key )
    {
        return Decryptor( key );
    }

    Encryptor BuildEncryptor( PublicKey key )
    {
        return Encryptor( key );
    }


/* These are replaced by directly building an Encryptor/Decryptor
 when you have a public or private key.

    void SetPublicKey (const PublicKey &pk)
    {
        this->pk = pk;
    }

    const PublicKey &GetPublicKey () const
    {
        return this->pk;
    }
*/

};

int main ()
{
    /* scenario 1: */
    PublicKeyCryptoProvider cryptoProvider;
    auto keys = cryptoProvider.GenerateKeys();
    auto decryptor = cryptoProvider.BuildDecryptor(keys.private_key);
    auto encryptor = cryptoProvider.BuildEncryptor(keys.public_key);

    std::cout << decryptor.Decrypt( encryptor.Encrypt(3) ) << std::endl;
    /* /scenario 1: */

    /* scenario 2: */
    PublicKeyCryptoProvider cryptoProvider1;
    auto keys1 = cryptoProvider1.GenerateKeys();

    PublicKeyCryptoProvider cryptoProvider2;
    auto encryptor2 = cryptoProvider2.BuildEncryptor(keys.public_key);

    int ciphertext = encryptor2.Encrypt(3);
    std::cout << decryptor.Decrypt(ciphertext) << std::endl;

    // I Can't do anything bad - the API has protected me from doing bad things! Yeah!
    //std::cout << cryptoProvider2.Decrypt(ciphertext) << std::endl;
    /* /scenario 2: */

    return 0;
}

答案 4 :(得分:0)

如果你必须延迟对象初始化,我建议如果未初始化代理,则使用带有访问运算符的代理。随时初始化代理。您不需要检查每个方法,但检查是否移动到代理。 一些智能指针会很方便。但是,据我所知,如果包含指针未初始化,它们不会抛出。因此,您可能需要自己的一个,如下所示。

#include <iostream>

class Workhorse
{
public:
    void SetData (const int &data)
    {
        this->data = data;
    }

    int GetData () const
    {
        return this->data;
    }

private:
    int data;
};

template <typename T> class Proxy
{
public:

    Proxy() : myObject(0)
    {
    }
    Proxy(T* anObj) : myObject(anObj)
    {
    }
    ~Proxy()
    {
        delete myObject;
        myObject = 0;
    }
    T* operator ->()const
    {
        if(NULL == myObject)
        {
            throw;  //  Bad object. Substitute an appropriate exception code.
        }
        return myObject;
    }

private:
    T* myObject;
};

class Worker
{
public:
    Worker ()
    {
    }
    ~Worker ()
    {
    }
    void Initialize (const int &data)
    {
        horse = new Workhorse;
        horse->SetData(data);
    }
    void Action () const
    {
            // Here no need to check if the horse is initialized.
        std::cout << horse->GetData() << std::endl;
    }

private:
    Proxy<Workhorse> horse;
};