5

私は次の設計哲学を強く信じています。

1> サービスは、データが保存されている場所のできるだけ近くに実装する必要があります。

2> ゲッターとセッターは悪であり、慎重に使用する必要があります。

私はむしろ、ここで 2 つの議論について議論することはせず、それぞれに優位性があると仮定します。

ここに私が現在直面している課題があります。AComputer が A にいくつかのサービスを提供し、A がすべての基本的なデータ メンバーを保持する2 つのクラス (つまりAComputerと) があります。A

事実:システム設計上、AComputer内部で結合することはできません。A私が知っていたのは、計算がデータにとどまるべきであるという私のポイント 1> を壊してしまったことです。

Aからにデータを渡す場合、AComputer10 個 (およそ) の個々のパラメーターを渡す必要があるため、それを行うための構造を設計することをお勧めします。そうしないと、コンストラクター リストが狂ってしまいます。に格納されているデータのほとんどは、 に格納されてAComputerいるデータの直接コピーAです。AComputerの他の関数AComputerもこれらの変数を必要とするため、これらのデータを内部に格納することにしました。

これが質問です(APIのメンテナンスと変更を考慮したベストプラクティスを求めています):

1> pass-structure はどこで定義する必要がありますPassDataか?

2> struct に getter/setter を提供する必要がありPassDataますか?

私の質問を詳細に説明するために、次のようなサンプルコードを提供しました。そこから学ぶことができるように、同じ問題に対処している実際のオープンソース API を見つけることができるのが最善です。

PassData m_data;class で定義された private を見るとAComputer、意図的にこれを行っています。言い換えれば、 の基本的な実装を変更した場合、個々の変数または何か他のものにAComputer置き換えることはできますが、 のインターフェースを壊すことはできません。したがって、この設計では、 struct の getter/setter を提供しません。PassData m_data;PassDataPassData

ありがとうございました

class AComputer
{
public:
    struct PassData
    {   // int type just used as an illustration. Real data has different types,
        // such as double, data, string, enum, etc.
        // Note: they are not exact copies of variables from A but derived from them
        int m_v1;
        // from m_v1 to m_v10
        //...
        int m_v10;
    };

    // it is better to store the passed-in data since other functions also need it.
    AComputer(const PassData& pd) : m_data(pd) {}

    int GetCombinedValue() const
    { /* This function returns a value based the passed-in struct of pd */ }

private:
    PassData m_data;    
};

class A
{
private:
    int m_i1;
    // from m_i1 to m_i10
    // ...
    int m_i10;
    // from m_i11 to m_i20
    // ...
    int m_i20;

    boost::shared_ptr<AComputer> m_pAComputer;

public:
    A()
    {
        AComputer::PassData aData;
        // populate aData ...
        m_pAComputer = boost::shared_ptr<AComputer>(new AComputer(aData));
    }

    int GetCombinedValue() const
    {
        return m_pAComputer->GetCombinedValue();
    }
};
4

4 に答える 4

11

始める前にいくつかの点を明確にしたほうがよいと思います、とあなたは言いました:

プライベート PassData m_data; を見ると、クラス AComputer で定義されていますが、これは意図的に行います。つまり、AComputer の基になる実装を変更すると、PassData m_data を置き換えることができます。個々の変数などを使用しますが、PassData のインターフェイスを壊さないでください。

これは正しくありません。PassData はインターフェイスの一部です。AComputer のコンストラクターで PassData が必要なため、クライアント コードを壊さずに PassData を置き換えることはできません。PassData は実装の詳細ではありませんが、純粋なインターフェイスです。

明確化が必要な 2 番目のポイント:

2> ゲッターとセッターは悪であり、慎重に使用する必要があります。

正しい!しかし、POD (Plain-Old-Data struct) はさらに最悪であることを知っておく必要があります。getter と setter を使用するクラスの代わりに POD を使用する唯一の利点は、関数を記述する手間を省けることです。しかし、本当の問題は未解決のままです。あなたのクラスのインターフェースは扱いにくく、維持するのが非常に困難です。

設計は常に、さまざまな要件間のトレードオフです。

柔軟性の誤った感覚

あなたのライブラリは配布されており、多くのコードがあなたのクラスを使用しています。この場合、PassData の変更は劇的になります。実行時にわずかな代償を払うことができれば、インターフェースを柔軟にすることができます。たとえば、AComputer のコンストラクタは次のようになります。

AComputer(const std::map<std::string,boost::any>& PassData);

boost::any hereをご覧ください。ユーザーがマップを簡単に作成できるように、マップのファクトリを提供することもできます。

プロ

  • フィールドが不要になった場合、コードは変更されません。

短所

  • 小さなランタイム価格。
  • コンパイラのタイプ セーフ チェックを失います。
  • 関数が別の必須フィールドを必要とする場合、まだ問題があります。クライアント コードはコンパイルされますが、正しく動作しません。

全体として、このソリューションは良くありません。最終的には、元のソリューションの派手なバージョンにすぎません。

戦略パターン

struct CalculateCombinedValueInterface
{
   int GetCombinedValue()=0;
   virtual ~CalculateCombinedValueInterface(){}
};

class CalculateCombinedValueFirst : CalculateCombinedValueInterface
{
   public:
       CalculateCombinedValueFirst(int first):first_(first){}
       int GetCombinedValue(); //your implementation here
   private:
       //I used one field but you get the idea
       int first_;
};

クライアント コードは次のようになります。

CalculateCombinedValueFirst* values = new CalculateCombinedValueFirst(42);

boost::shared_ptr<CalculateCombinedValueInterface> data(values);

コードを変更する場合は、既にデプロイされているインターフェイスに触れないでください。これに対するオブジェクト指向のソリューションは、抽象クラスから継承する新しいクラスを提供することです。

class CalculateCombinedValueSecond : CalculateCombinedValueInterface
{
   public:
       CalculateCombinedValueFirst(int first,double second)
           :first_(first),second_(second){}
       int GetCombinedValue(); //your implementation here
   private:
       int first_;
       double second_;
};

クライアントは、新しいクラスにアップグレードするか、既存のバージョンを使用するかを決定します。

プロ

  • クライアント コードを中断することなく、インターフェイスを改善します。
  • 既存のコードには触れていませんが、新しいファイルに新しい機能を導入しています。
  • より細かい制御が必要な場合 は、テンプレート メソッド デザイン パターンを使用できます。

短所

  • 仮想関数を使用するオーバーヘッド (基本的に数ピコ秒!)
  • 既存のコードを壊すことはできません。既存のインターフェイスはそのままにして、新しいクラスを追加して異なる動作をモデル化する必要があります。

パラメータ数

1 つの関数に 10 個のパラメータ入力がある場合、これらの値は論理的に関連している可能性が非常に高くなります。これらの値の一部をクラスで収集できます。これらのクラスは、関数の入力となる別のクラスに結合される場合があります。クラスに 10 個 (またはそれ以上!) のデータ メンバーがあるという事実は、ベルを鳴らす必要があります。

単一責任の原則は、次のように述べています。

クラスを変更する理由は 1 つだけであってはなりません。

この原則の帰結は、クラスは小さくなければならないということです。クラスに 20 個のデータ メンバーがある場合、それを変更する多くの理由が見つかる可能性が非常に高くなります。

結論

インターフェイス (あらゆる種類のインターフェイス) をクライアントに提供した後は、それを変更することはできません (良い例は、コンパイラが何年にもわたって実装する必要がある C++ のすべての非推奨機能です)。暗黙的なインターフェースであっても、提供しているインターフェースに注意してください。あなたの例では、 PassData は実装の詳細ではありませんが、クラス インターフェイスの一部です。

パラメータの数は、設計をレビューする必要があることを示しています。大きなクラスを変更することは非常に困難です。クラスは小さく、インターフェイス (C++ スラングの抽象クラス) を介してのみ他のクラスに依存する必要があります。

あなたのクラスが次の場合:

1) 小さく、変更する理由が 1 つしかない

2) 抽象クラスから派生

3) 他のクラスは、抽象クラスへのポインタを使用してそれを参照します

コードは簡単に変更できます (ただし、既に提供されているインターフェイスは保持する必要があります)。

これらの要件をすべて満たしていないと、問題が発生します。

注:要件 2) および 3) は、動的ポリモーフィムを提供する代わりに、設計が静的ポリモーフィムを使用している場合に変更される可能性があります。

于 2012-05-04T11:42:01.963 に答える
0

パターンオブジェクトを使用するようにリファクタリングを検討することもできます。このオブジェクトの唯一の目的は、メソッド呼び出しのパラメーターを含めることです。詳細については、http ://sourcemaking.com/refactoring/introduce-parameter-objectをご覧ください。

于 2012-04-30T18:14:22.610 に答える
0

すべての AComputer クライアントを完全に制御できる場合は、10 個の引数の代わりに PassData を使用することをお勧めします。これには 2 つの利点があります。渡すデータの別の部分を追加するときに必要な変更が少なくて済みます。また、代入を使用して呼び出し元サイトのメンバーを構造化し、各 "引数" の意味を明確にすることができます。

ただし、他の人が AComputer を使用する場合、PassData の使用には重大な欠点があります。これがないと、11 番目の引数を AComputer コンストラクターに追加すると、コンパイラーは、実引数リストを更新しなかったユーザーに対してエラーを検出します。11 番目のメンバーを PassData に追加すると、コンパイラは、新しいメンバーがゴミである構造体、または最良の場合はゼロである構造体を黙って受け入れます。

私の意見では、PassData を使用する場合、ゲッターとセッターを使用するのはやり過ぎです。Sutter と Alexandresku による「C++ Coding Standards」はそれに同意します。項目 #41 のタイトルは次のとおりです

于 2012-05-08T14:36:13.907 に答える
0

通常のクラス設計では、データ メンバーにアクセスできるように、すべてのメンバー関数に this ポインターが暗黙のパラメーターとして渡されます。

// Regular class
class SomeClass
{
public:
    // will be name-mangled by the compiler as something like: 
    // void SomeClass_getValue(const SomeClass*) const;
    void getValue() const 
    {
        return value_; // actually: return this->value_;
    }

private:
    int value_;
};

これを可能な限り真似する必要があります。何らかの理由で AComputer クラスと A クラスを 1 つのクリーンなクラスにマージすることが許可されていない場合、次善の策は、AComputer が A へのポインターをデータ メンバーとして取得できるようにすることです。AComputer のすべてのメンバー関数で、A の getter/setter 関数を明示的に使用して、関連するデータ メンバーにアクセスする必要があります。

class AComputer
{
public:
    AComputer(A* a): p_(a) {}

    // this will be mangled by the compiler to something like
    // AComputer_GetCombinedValue(const Acomputer*) const;
    int GetCombinedValue() const
    {
         // in a normal class it would be: return m_i1 + m_i2 + ...
         // which would actually be: return this->m_i1 + this->m_i12 + ...
         // the code below actually is: return this->p_->m_i1 + this->p_->m_i2 + ... 
         return p_->get_i1() + p_->get_i2() + ...       
    }

private:
    class A;
    A* p_;
};

class A
{
public:
   // setters and getters

private:
   // data only, NO pointer to AComputer object
}

したがって、事実上、AComputer と A が同じ抽象化の一部であるという錯覚をユーザーに作成する余分なレベルの間接性を作成しました。

于 2012-05-04T11:44:34.490 に答える