3

コンストラクターですべての内部メンバーを初期化できないクラスを設計する方法を理解するのに苦労しています。これが基本的なものであり、ネット上で議論されるべきであることはわかっていますが、何を探すべきかわかりません。したがって、たとえば、次のコードを検討してください。

#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 クラスに isInitialized フラグを追加し、Initialize() でそれを true に設定し、各パブリック メソッドの先頭でテストすることです (継承を導入する場合は、プロテクト / プライベート メソッドでも同様でしょうか?) . 残念ながら、これは少し面倒で、維持するのが難しいようです。また、すべてのメソッドで if ステートメントを繰り返すのはひどいことです。スレッド セーフの問題について考え始めたわけではありませんが、現在はシングル スレッドのアプリケーションしか実装していません。これを設計するよりスマートな方法はありますか?


編集:OK、私は例としてダムデザインを選びましたが、実際には欠陥があります. 私が持っているものをより明確に示してみましょう:

#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 フラグを追加するよりも良いオプションはありますか? これは非常に単純な例であることに言及する必要があります。私の場合、PublicKeyCryptoProvider が秘密鍵の所有者であり、公開メソッドがはるかに多い場合、PublicKeyCryptoProvider はより高速な暗号化を実行できるため、フラグをテストする運命にあるからです。数回以上...また、サーバーがクライアント用のパブリックメソッドの束を公開するクライアント-サーバーモックアップシナリオがありますが、クライアントは Initialize() メソッドを呼び出した後にのみメソッドを呼び出すことができますサーバー...

4

5 に答える 5

2

あなたが興味を持っている動作には、Workhorse の関数の 1 つまたは代わりにダミー関数へのアクセスを提供するかどうかを決定する、マネージャーとして機能するクラスが必要になるようです。1 つの可能性は、Workhorse のインターフェースを指定する抽象親クラス (Horse) を作成することですが、どの関数も実装しません。そこから Workhorse と TrojanHorse の 2 つのクラスを派生させます。TrojanHorse は、親クラスのすべての関数をシェルとして実装します。Workhorse は、作成済みのとおりです。

manager クラスには、関心のある初期化関数を含めることができ、Horse 型のオブジェクトを格納できます。デフォルトでは、horse オブジェクトは TrojanHorse オブジェクトに割り当てられますが、initialize では代わりに Workhorse オブジェクトに割り当てられます。

このソリューションは、if ステートメントによる速度への影響のほとんどすべてを回避し、クラスが適切な方法で変更されていない場合にコンパイラーがエラーを出すという意味で維持可能であり、別のプログラマーが見ている場合でも理解可能です。コード。

于 2012-06-30T19:08:57.933 に答える
2

私は次のことをします:

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

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

private:
    Workhorse horse;
};

初期化せずに Worker オブジェクトを存在させたくないのは明らかなので、その初期化はその構築の一部である必要があり、この初期化なしでは機能しないため、この初期化なしでインスタンス化する必要があります。

于 2012-06-27T10:25:33.267 に答える
1

素晴らしい質問です。使いにくい API を間違って作成することは常に良いことです。また、完全に構築されていないクラスは危険であり、正しく使用するのが難しく、間違って使用するのは簡単です。彼らは自分自身と他の人を失敗に追い込んでいます。「何か悪いことをする」コードさえ許可しない、より安全な設計を考え出すために、2番目の例でいくつかのリファクタリングを行いました。

一般的な考え方は、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;
}
于 2012-07-01T23:32:25.713 に答える
1

あなたは継承が進むべき道だとは思わなかったと言いましたが、最小限の継承でこれを行うかなりきれいな方法があります。

ここでは、いくつかの設計パターンが役立ちます。インターフェースを実装から切り離し、実装を「常にエラーを返す」「何か役に立つことをする」と考えると、これら 2 つの実装を戦略として、インターフェースをプロキシとして見ることができます。

プロキシは常に実装への呼び出しを転送し、常に実装があります (フラグをチェックする必要はありません)。

インターフェイスは、何らかのエラー (アサート、スローなど) を引き起こすデフォルトの実装で初期化されます。これは一例です

これは、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;
}
于 2012-07-01T06:15:32.620 に答える
0

オブジェクトの初期化を遅らせる必要がある場合は、プロキシが初期化されていない場合にアクセス オペレーターをスローするプロキシの使用を提案します。必要なときにいつでもプロキシを初期化します。各メソッドの if チェックは必要ありませんが、プロキシに移動したかどうかをチェックします。いくつかのスマートポインターが便利だったでしょう。しかし、私が知る限り、含まれているポインターが初期化されていない場合、それらはスローしません。したがって、以下に示すように、独自のものが必要になる場合があります。

#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;
};
于 2012-07-02T11:16:12.797 に答える