dynamic_castの使用を回避するための適切な設計?


9

いくつかの調査を行った後、私が頻繁に遭遇する問題を解決する簡単な例を見つけることができないようです。

Squares、Circles、およびその他の形状を作成し、画面に表示し、それらを選択した後でそれらのプロパティを変更して、すべての周囲を計算できる小さなアプリケーションを作成したいとします。

私はこのようなモデルクラスを行います:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    SHAPE_TYPE getType() const{return m_type;}
protected :
    const SHAPE_TYPE  m_type;
};

class Square : public AbstractShape
{
public:
    Square():AbstractShape(SQUARE){}
    ~Square();

    void setWidth(float w){m_width = w;}
    float getWidth() const{return m_width;}

    float computePerimeter() const{
        return m_width*4;
    }

private :
    float m_width;
};

class Circle : public AbstractShape
{
public:
    Circle():AbstractShape(CIRCLE){}
    ~Circle();

    void setRadius(float w){m_radius = w;}
    float getRadius() const{return m_radius;}

    float computePerimeter() const{
        return 2*M_PI*m_radius;
    }

private :
    float m_radius;
};

(三角形、六角形、形状変数のプロパティと関連するゲッターとセッターが増えるたびに、形状のクラスが増えると想像してください。直面した問題には8つのサブクラスがありましたが、例のために2で停止しました)

これShapeManagerで、すべてのシェイプをインスタンス化して配列に格納しました。

class ShapeManager
{
public:
    ShapeManager();
    ~ShapeManager();

    void addShape(AbstractShape* shape){
        m_shapes.push_back(shape);
    }

    float computeShapePerimeter(int shapeIndex){
        return m_shapes[shapeIndex]->computePerimeter();
    }


private :
    std::vector<AbstractShape*> m_shapes;
};

最後に、各タイプの形状の各パラメーターを変更するためのスピンボックス付きのビューがあります。たとえば、画面で四角形を選択すると、パラメーターウィジェットにはにSquare関連するパラメーターのみが表示され(のおかげでAbstractShape::getType())、四角形の幅を変更するように提案されます。これを行うには、で幅を変更できる関数が必要です。ShapeManagerこれが私が行う方法です。

void ShapeManager::changeSquareWidth(int shapeIndex, float width){
   Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
   assert(square);
   square->setWidth(width);
}

私が持つ可能性のあるサブクラス変数ごとにを使用しdynamic_castてゲッター/セッターのカップルを実装することを回避するより良いデザインはありShapeManagerますか?私はすでにテンプレートを使用しようとしましたが、失敗しました


私が直面してる問題がシェイプではなくて、実際にはない別のJobS:3Dプリンタ用(EX PrintPatternInZoneJobTakePhotoOfZoneとの、など)AbstractJob彼らの基本クラスとして。仮想メソッドはexecute()ありませんgetPerimeter()具体的な使用法を使用する必要があるのは、ジョブが必要とする特定の情報を入力するときだけです。

  • PrintPatternInZone 印刷するポイントのリスト、ゾーンの位置、温度などのいくつかの印刷パラメーターが必要です

  • TakePhotoOfZone 写真に取り込むゾーン、写真が保存されるパス、寸法などが必要です...

次にを呼び出すexecute()と、ジョブは、実行する必要があるアクションを実現するために必要な特定の情報を使用します。

ジョブの具象タイプを使用する必要があるのは、これらの情報を入力または表示するときのみです(a TakePhotoOfZone Jobが選択されている場合、ゾーン、パス、および寸法パラメーターを表示および変更するウィジェットが表示されます)。

次に、JobsはJob、最初のジョブを実行するsのリストに入れられ、(を呼び出すことによってAbstractJob::execute())それを実行し、次のジョブに進み、リストの最後まで続きます。(これが継承を使用する理由です)。

さまざまなタイプのパラメータを保存するには、JsonObject次のように使用します。

  • 利点:どのジョブでも同じ構造、パラメーターの設定または読み取り時にdynamic_castなし

  • 問題:(PatternまたはへのZone)ポインタを格納できません

データを保存するより良い方法があると思いますか?

次に、そのタイプの特定のパラメータを変更する必要があるときに、それを使用するために具体的なタイプをどのように保存しJobますか?JobManagerのリストしかありませんAbstractJob*


5
ShapeManagerは基本的にすべてのタイプの形状のすべてのセッターメソッドを含むため、ShapeManagerはGodクラスになるようです。
Emerson Cardoso 2018年

「プロパティバッグ」のデザインを検討しましたか?などchangeValue(int shapeIndex, PropertyKey propkey, double numericalValue)ここでPropertyKey列挙または文字列、及び許容値のいずれか一つである(セッターへの呼び出しが幅の値を更新することを意味する)「幅」とすることができます。
18年

プロパティバッグはオブジェクト指向のアンチパターンと見なされている場合もありますが、プロパティバッグを使用するとデザインが単純化される場合があり、他のすべての方法では状況が複雑になります。ただし、プロパティバッグがユースケースに適しているかどうかを判断するには、より多くの情報(GUIコードがゲッター/セッターと対話する方法など)が必要です。
18年

プロパティバッグのデザインを検討しましたが(名前はわかりませんでした)、JSONオブジェクトコンテナーを使用しました。それは確かにうまくいくかもしれませんが、それはエレガントなデザインではなく、より良いオプションが存在するかもしれないと思いました。なぜそれがOOアンチパターンと見なされるのですか?
ElevenJune

たとえば、後で使用するためにポインタを保存したい場合、どうすればよいですか?
ElevenJune

回答:


10

エマソンカルドソの「その他の提案」を拡張したいと思います。これは、一般的なケースでは正しいアプローチであると信じているためです。

問題

あなたの例では、AbstractShapeクラスにはgetType()基本的に具象型を識別するメソッドがあります。これは通常、抽象化が不十分であることを示しています。抽象化の要点は、結局のところ、具体的な型の詳細を気にする必要がないということです。

また、慣れていない場合は、オープン/クローズの原則を確認する必要があります。形状の例でよく説明されているので、くつろいでいただけます。

有用な抽象化

AbstractShape何かに役立つと思ったので紹介したと思います。ほとんどの場合、アプリケーションの一部は、形状が何であるかに関係なく、形状の周囲を知る必要があります。

これは抽象化が意味をなす場所です。このモジュールは具体的な形状に関係しないため、依存できるAbstractShapeだけです。同じ理由で、getType()メソッドは必要ありません-したがって、それを取り除く必要があります。

アプリケーションの他の部分は、特定の種類の形状でのみ機能しRectangleます。これらの領域はAbstractShapeクラスの恩恵を受けないので、そこでは使用しないでください。これらのパーツに正しい形状のみを渡すには、コンクリート形状を個別に保存する必要があります。(AbstractShape追加として保存することも、オンザフライで結合することもできます)。

コンクリートの使用を最小限に抑える

それを回避する方法はありません。少なくともいくつかの場所でコンクリートタイプが必要です-少なくとも建設中。ただし、具体的な型の使用をいくつかの明確に定義された領域に限定しておくことが最善の場合もあります。これらの個別の領域は、さまざまなタイプを処理するという唯一の目的を持っていますが、すべてのアプリケーションロジックはそれらの領域から除外されています。

これをどのように達成しますか?通常、より多くの抽象化を導入することによって-既存の抽象化をミラーリングする場合とミラーリングしない場合があります。たとえば、GUIはどのような形状を扱っているかを実際に知る必要はありませ。画面上にユーザーが図形を編集できる領域があることを知る必要があるだけです。

あなたは抽象的に定義してShapeEditViewいるために、あなたが持っているRectangleEditViewCircleEditView幅/高さまたは半径の実際のテキストボックスを保持する実装。

最初のステップでは、を作成するRectangleEditViewたびにを作成しRectangle、それをに入れることができますstd::map<AbstractShape*, AbstractShapeView*>。必要に応じてビューを作成したい場合は、代わりに次のようにします。

std::map<AbstractShape*, std::function<AbstractShapeView*()>> viewFactories;
// ...
auto rect = new Rectangle();
// ...
auto viewFactory = [rect]() { return new RectangleEditView(rect); }
viewFactories[rect] = viewFactory;

どちらの方法でも、この作成ロジック外のコードは具体的な形状を処理する必要はありません。形状の破壊の一部として、明らかにファクトリを削除する必要があります。もちろん、この例は過度に単純化されていますが、アイデアが明確であることを願っています。

適切なオプションの選択

非常に単純なアプリケーションでは、ダーティー(キャスティング)ソリューションは、費用に見合うだけの効果があることに気付くでしょう。

具象タイプごとに個別のリストを明示的に維持することは、アプリケーションが主に具象形状を扱うが、普遍的ないくつかの部分がある場合におそらく行く方法です。ここでは、共通の機能で必要な場合にのみ抽象化することは理にかなっています。

形状を操作する多くのロジックがあり、正確な種類の形状が実際にはアプリケーションの詳細である場合、通常はすべての方法で効果があります。


私はあなたの答えが本当に好きです、あなたは問題を完全に説明しました。私が直面している問題は、実際にはShapesではなく、AbstractJobを基本クラスとする3Dプリンター(例:PrintPatternInZoneJob、TakePhotoOfZoneなど)のさまざまなジョブに関するものです。仮想メソッドは、getPerimeter()ではなく、execute()です。具体的な使用法を使用する必要があるのは、ジョブが必要とする特定の情報(ポイント、位置、温度などのリスト)を特定のウィジェットで埋めるときだけです。各ジョブにビューをアタッチすることは、この特定のケースでは行うべきことではないようですが、あなたのビジョンを私のpbに適応させる方法がわかりません。
ElevenJune

個別のリストを保持したくない場合は、viewFactory:ではなくviewSelectorを使用できます[rect, rectView]() { rectView.bind(rect); return rectView; }。ちなみに、これはもちろんRectangleCreatedEventHandlerなどのプレゼンテーションモジュールで行う必要があります。
doubleYou 2018年

3
これが言われている、これを過度に設計しないようにしてください。抽象化のメリットは、追加の配管のコストを上回ります。場合によっては、適切に配置されたキャスト、または個別のロジックが望ましい場合があります。
doubleYou 2018年

2

1つのアプローチは、特定の型へのキャストを回避するために、より一般的なものにすることです

プロパティ名の特定のキーに基づいてマップに値を設定する、基本クラスの「dimension」フロートプロパティの基本的なゲッター/セッターを実装できます。以下の例:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    void setDimension(const std::string& name, float v){ m_dimensions[name] = v; }
    float getDimension() const{ return m_dimensions[name]; }

    SHAPE_TYPE getType() const{return m_type;}

protected :
    const SHAPE_TYPE  m_type;
    std::map<std::string, float> m_dimensions;
};

次に、マネージャークラスでは、以下のように1つの関数のみを実装する必要があります。

void ShapeManager::changeShapeDimension(const int shapeIndex, const std::string& dimension, float value){
   m_shapes[shapeIndex]->setDimension(name, value);
}

ビュー内での使用例:

ShapeManager shapeManager;

shapeManager.addShape(new Circle());
shapeManager.changeShapeDimension(0, "RADIUS", 5.678f);
float circlePerimeter = shapeManager.computeShapePerimeter(0);

shapeManager.addShape(new Square());
shapeManager.changeShapeDimension(1, "WIDTH", 2.345f);
float squarePerimeter = shapeManager.computeShapePerimeter(1);

別の提案:

マネージャーはセッターと境界の計算(Shapeによっても公開されます)のみを公開するため、特定のShapeクラスをインスタンス化するときに、適切なビューをインスタンス化するだけで済みます。例えば:

  • SquareとSquareEditViewをインスタンス化します。
  • SquareインスタンスをSquareEditViewオブジェクトに渡します。
  • (オプション)ShapeManagerを使用する代わりに、メインビューで形状のリストを保持できます。
  • SquareEditView内では、Squareへの参照を保持します。これにより、オブジェクトを編集するためにキャストする必要がなくなります。

私は最初の提案を気に入って、すでにそれについて考えましたが、さまざまな変数(float、ポインター、配列)を格納する場合は、かなり制限があります。2番目の提案では、正方形が既にインスタンス化されている場合(ビューでそれを好んでいました)、それがSquare *オブジェクトであることをどのようにして確認しますか?形状を格納するリストはAbstractShape *を返します。
ElevenJune

@ElevenJune-はい、すべての提案には欠点があります。最初に、より多くのタイプのプロパティが必要な場合は、単純なマップではなく、より複雑なものを実装する必要があります。2番目の提案は、形状の格納方法を変更します。リストにベースシェイプを格納しますが、同時にビューへの特定のシェイプの参照を提供する必要があります。シナリオの詳細を提供していただければ、これらのアプローチが単にdynamic_castを実行するよりも優れているかどうかを評価できます。
Emerson Cardoso

@ElevenJune-ビューオブジェクトを持つことの要点は、GUIがそれがSquareタイプのクラスで動作していることを知る必要がないようにすることです。ビューオブジェクトは、オブジェクトを「表示」するために必要なもの(ユーザーが定義するものは何でも)を提供し、内部的には、Squareクラスのインスタンスを使用していることを認識しています。GUIはSquareViewインスタンスとのみ対話します。したがって、「Square」クラスをクリックすることはできません。SquareViewクラスのみクリックできます。SquareViewのパラメータを変更すると....基礎となる広場クラスを更新します
ダンク

...このアプローチでは、ShapeManagerクラスを取り除くことができます。これにより、ほぼ確実に設計が簡略化されます。クラスをマネージャーと呼んだ場合、それは悪いデザインだと思い、何か他のことを理解します。マネージャークラスは無数の理由で悪いです。特に、神クラスの問題と、クラスが実際に何をしているのか誰も知らないということは、マネージャーが管理しているものに接線的に関連することさえ何でもできるので、実行できません。あなたがフォローしている開発者は、それを利用して典型的な泥沼につながります。
2018

1
...すでにその問題に遭遇しています。マネージャーが形の次元を変える人であることが一体なぜ意味があるのでしょうか?マネージャーがシェイプの周囲を計算するのはなぜですか?あなたがそれを理解していない場合に備えて、私は「別の提案」が好きです。
2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.