いくつかの調査を行った後、私が頻繁に遭遇する問題を解決する簡単な例を見つけることができないようです。
Square
s、Circle
s、およびその他の形状を作成し、画面に表示し、それらを選択した後でそれらのプロパティを変更し、それらのすべてを計算できる小さなアプリケーションを作成したいとします境界。
私はこのようなモデルクラスを行います:
_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
にゲッター/セッターカップルを実装しないようにするより良い設計はありますか?私はすでに テンプレートですが失敗しました を使用しようとしました。
私が直面している問題は、実際にはShapesではなく、3DプリンターのJob
sが異なる(例:PrintPatternInZoneJob
、TakePhotoOfZone
など)をAbstractJob
を基本クラスとして使用します。仮想メソッドはexecute()
ではなくgetPerimeter()
です。 具体的な使用法を使用する必要があるのは、ジョブが必要とする特定の情報を入力するときだけです:
PrintPatternInZone
には、印刷するポイントのリスト、ゾーンの位置、温度などのいくつかの印刷パラメーターが必要です
TakePhotoOfZone
には、写真に取り込むゾーン、写真が保存されるパス、寸法などが必要です...
次にexecute()
を呼び出すと、ジョブは、実行することになっているアクションを実現するために必要な特定の情報を使用します。
ジョブの具象タイプを使用する必要があるのは、これらの情報を入力または表示するときだけです(TakePhotoOfZone
Job
を選択すると、ゾーン、パス、および寸法パラメーターを表示および変更するウィジェットが表示されます)。
次にJob
sがJob
sのリストに入れられ、最初のジョブを実行して(AbstractJob::execute()
を呼び出して)ジョブを実行し、リストの終わり。 (これが継承を使用する理由です)。
さまざまなタイプのパラメーターを格納するにはJsonObject
を使用します:
利点:どのジョブでも同じ構造、パラメーターの設定または読み取り時にdynamic_castなし
問題:(Pattern
またはZone
への)ポインターを格納できません
データを保存するより良い方法があると思いますか?
次に、Job
の具体的なタイプをどのように保存して、そのタイプの特定のパラメータを変更する必要があるときに使用しますか? JobManager
には、_AbstractJob*
_のリストしかありません。
エマソンカルドソの「その他の提案」を拡張したいと思います。これは、一般的なケースでは正しいアプローチであると信じているからです。
問題
あなたの例では、AbstractShape
クラスには、基本的に具象型を識別するgetType()
メソッドがあります。これは通常、抽象化が不十分であることを示しています。結局のところ、抽象化の全体のポイントは、具体的な型の詳細を気にする必要がないということです。
また、慣れていない場合は、オープン/クローズの原則をよく読んでください。形状の例でよく説明されているので、くつろいでいただけます。
便利な抽象化
何かに便利だと思ったので、AbstractShape
を導入したと思います。ほとんどの場合、アプリケーションの一部は、形状が何であるかに関係なく、形状の周囲を知る必要があります。
これは抽象化が意味をなす場所です。このモジュールは具体的な形状に関係しないため、AbstractShape
のみに依存できます。同じ理由で、それはgetType()
メソッドを必要としないので、あなたはそれを取り除くべきです。
アプリケーションの他の部分は、特定の種類の形状でのみ機能します。 Rectangle
。これらの領域はAbstractShape
クラスの恩恵を受けないため、そこで使用しないでください。これらのパーツに正しい形状のみを渡すには、コンクリート形状を個別に保存する必要があります。 (それらをAbstractShape
として追加で保存するか、オンザフライで結合できます)。
コンクリート使用量の最小化
それを回避する方法はありません。少なくともいくつかの場所でコンクリートタイプが必要です-少なくとも建設中。ただし、具体的な型の使用をいくつかの明確に定義された領域に限定しておくことが最善の場合もあります。これらの個別の領域は、さまざまなタイプを処理するという唯一の目的を持っていますが、すべてのアプリケーションロジックはそれらの領域から除外されています。
これをどのように達成しますか?通常、より多くの抽象化を導入することにより、既存の抽象化を反映する場合と反映しない場合があります。たとえば、GUIはreallyがどのような形状を扱っているかを知る必要はありません。画面上にユーザーが図形を編集できる領域があることを知っているだけです。
したがって、幅/高さまたは半径の実際のテキストボックスを保持するShapeEditView
およびRectangleEditView
実装がある抽象CircleEditView
を定義します。
最初のステップでは、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;
どちらの方法でも、この作成ロジックの外のコードは具体的な形状を処理する必要はありません。形状の破壊の一環として、明らかにファクトリを削除する必要があります。もちろん、この例は非常に単純化されていますが、その考えが明確であることを願っています。
正しいオプションの選択
非常に単純なアプリケーションでは、ダーティー(キャスティング)ソリューションが最も効果的であることがわかります。
具象タイプごとに個別のリストを明示的に維持することは、アプリケーションが主に具象形状を扱うが、普遍的ないくつかの部分がある場合におそらく行く方法です。ここでは、共通の機能で必要な場合にのみ抽象化することは理にかなっています。
形状を操作する多くのロジックがあり、正確な種類の形状が実際にはアプリケーションの詳細である場合は、通常、すべての方法で効果があります。
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クラスをインスタンス化するときに、適切なビューをインスタンス化するだけで済みます。例えば: