การออกแบบที่เหมาะสมเพื่อหลีกเลี่ยงการใช้ 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)

ตอนนี้ฉันมี a ShapeManager, instantiating และเก็บรูปร่างทั้งหมดในอาร์เรย์:

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และเพื่อใช้คู่ getter / setter ในShapeManagerตัวแปรย่อยแต่ละคลาสที่ฉันอาจมี? ฉันพยายามแล้วที่จะใช้แม่แบบ แต่ล้มเหลว


ปัญหาที่เกิดขึ้นฉันหันหน้าไปไม่ได้จริงๆกับรูปร่าง แต่มีความแตกต่างกันJobsสำหรับเครื่องพิมพ์ 3D (เช่นPrintPatternInZoneJob, TakePhotoOfZoneฯลฯ ) ด้วยAbstractJobเป็นชั้นฐานของพวกเขา วิธีเสมือนและไม่ได้execute() ครั้งเดียวที่ฉันต้องใช้การใช้งานที่เป็นรูปธรรมคือการกรอกข้อมูลเฉพาะที่งานต้องการ :getPerimeter()

  • PrintPatternInZone ต้องการรายการจุดที่จะพิมพ์ตำแหน่งของโซนพารามิเตอร์การพิมพ์บางอย่างเช่นอุณหภูมิ

  • TakePhotoOfZone ต้องการโซนที่จะถ่ายภาพเส้นทางที่จะบันทึกรูปภาพขนาด ฯลฯ ...

เมื่อฉันจะโทรหาexecute()งานจะใช้ข้อมูลเฉพาะที่พวกเขาต้องตระหนักถึงการกระทำที่พวกเขาควรจะทำ

ครั้งเดียวที่ฉันต้องใช้งานรูปธรรมของงานคือเมื่อฉันกรอกหรือแสดงข้อมูลเหล่านี้ (หากTakePhotoOfZone Jobเลือก a, วิดเจ็ตที่แสดงและแก้ไขพารามิเตอร์โซน, พา ธ และมิติข้อมูล)

Jobs จะใส่แล้วลงในรายการของJobs ซึ่งใช้งานครั้งแรกที่รัน (โดยการเรียกAbstractJob::execute()) ไปถัดไปบนและบนจนกว่าจะสิ้นสุดของรายการ (นี่คือเหตุผลที่ฉันใช้มรดก)

ในการจัดเก็บพารามิเตอร์ประเภทต่างๆฉันใช้JsonObject:

  • ข้อได้เปรียบ: โครงสร้างเดียวกันสำหรับงานใด ๆ ไม่มี dynamic_cast เมื่อตั้งค่าหรืออ่านพารามิเตอร์

  • ปัญหา: ไม่สามารถจัดเก็บพอยน์เตอร์ (ถึงPatternหรือZone)

คุณมีวิธีที่ดีกว่าในการจัดเก็บข้อมูลหรือไม่?

จากนั้นคุณจะเก็บประเภทคอนกรีตของการJobใช้เมื่อฉันต้องแก้ไขพารามิเตอร์เฉพาะของประเภทนั้นอย่างไร JobManagerเพียง AbstractJob*แต่มีรายชื่อของ


5
ดูเหมือนว่า ShapeManager ของคุณจะกลายเป็นคลาสของพระเจ้าเพราะโดยพื้นฐานแล้วมันจะมีวิธีตั้งค่าทั้งหมดสำหรับรูปร่างทุกประเภท
Emerson Cardoso

คุณคิดว่าเป็น "ถุงสมบัติ" หรือไม่? เช่นchangeValue(int shapeIndex, PropertyKey propkey, double numericalValue)ที่PropertyKeyสามารถเป็น enum หรือสตริงและ "ความกว้าง" (ซึ่งหมายความว่าการเรียกไปยัง setter จะอัปเดตค่าความกว้าง) เป็นหนึ่งในค่าที่อนุญาต

แม้ว่าจะมีการพิจารณาถึงรูปแบบการต่อต้านถุงของ OO แต่ก็มีบางสถานการณ์ที่การใช้กระเป๋าคุณสมบัติทำให้การออกแบบง่ายขึ้นซึ่งทุก ๆ ทางเลือกอื่น ๆ จะทำให้สิ่งต่าง ๆ มีความซับซ้อนมากขึ้น แม้ว่าในการพิจารณาว่าถุงคุณสมบัติเหมาะสมกับกรณีการใช้งานของคุณหรือไม่จำเป็นต้องมีข้อมูลเพิ่มเติม (เช่นวิธีการใช้รหัส GUI โต้ตอบกับผู้ใช้ / setter)

ฉันพิจารณาการออกแบบถุงสมบัติ (แม้ว่าฉันไม่ทราบชื่อ) แต่ใช้คอนเทนเนอร์วัตถุ JSON มันแน่ใจว่าใช้งานได้ แต่ฉันคิดว่ามันไม่ใช่การออกแบบที่หรูหราและอาจมีตัวเลือกที่ดีกว่า เหตุใดจึงถือว่าเป็นรูปแบบการต่อต้าน OO
ElevenJune

ตัวอย่างเช่นหากฉันต้องการเก็บตัวชี้ไว้เพื่อใช้ในภายหลังฉันจะทำอย่างไร
ElevenJune

คำตอบ:


10

ฉันต้องการขยาย "คำแนะนำอื่น ๆ " ของ Emerson Cardoso เพราะฉันเชื่อว่านี่เป็นแนวทางที่ถูกต้องในกรณีทั่วไป - ถึงแม้ว่าคุณอาจจะหาวิธีแก้ปัญหาอื่นที่เหมาะสมกับปัญหาใด ๆ ก็ตาม

ปัญหา

ในตัวอย่างของคุณAbstractShapeคลาสมีgetType()วิธีการที่ระบุประเภทที่เป็นรูปธรรม นี่เป็นสัญญาณบ่งบอกว่าคุณไม่มีความคิดที่ดี จุดที่เป็นนามธรรมทั้งหมดไม่ต้องสนใจรายละเอียดของประเภทคอนกรีต

ในกรณีที่คุณไม่คุ้นเคยคุณควรอ่านหลักการเปิด / ปิด มันมักจะอธิบายด้วยตัวอย่างรูปร่างดังนั้นคุณจะรู้สึกเหมือนอยู่บ้าน

บทคัดย่อที่เป็นประโยชน์

ฉันถือว่าคุณได้แนะนำAbstractShapeเพราะคุณพบว่ามีประโยชน์สำหรับบางสิ่ง เป็นไปได้มากที่บางส่วนของแอปพลิเคชันของคุณจำเป็นต้องรู้ขอบเขตของรูปร่างโดยไม่คำนึงถึงรูปร่าง

นี่คือสถานที่ที่เป็นนามธรรมทำให้รู้สึก เนื่องจากโมดูลนี้ไม่เกี่ยวข้องกับรูปร่างที่เป็นรูปธรรมจึงสามารถพึ่งพาได้AbstractShapeเท่านั้น ด้วยเหตุผลเดียวกันมันไม่จำเป็นต้องใช้getType()วิธีการดังนั้นคุณควรกำจัดมัน

ส่วนอื่น ๆ Rectangleของแอพลิเคชันจะทำงานกับชนิดหนึ่งของรูปร่างเช่น พื้นที่เหล่านั้นจะไม่ได้รับประโยชน์จากAbstractShapeชั้นเรียนดังนั้นคุณไม่ควรใช้ที่นั่น ในการที่จะส่งเฉพาะรูปร่างที่ถูกต้องไปยังชิ้นส่วนเหล่านี้คุณจะต้องจัดเก็บรูปทรงคอนกรีตแยกต่างหาก (คุณสามารถจัดเก็บเป็นAbstractShapeเพิ่มเติมหรือรวมเข้าด้วยกันได้ทันที)

การลดการใช้คอนกรีต

ไม่มีวิธีแก้ไข: คุณต้องการประเภทคอนกรีตในบางแห่ง - อย่างน้อยที่สุดระหว่างการก่อสร้าง อย่างไรก็ตามบางครั้งก็เป็นการดีที่สุดที่จะใช้งานคอนกรีตในบางพื้นที่ที่ จำกัด พื้นที่แยกเหล่านี้มีจุดประสงค์เพียงอย่างเดียวในการจัดการกับประเภทที่แตกต่างกัน - ในขณะที่ตรรกะการใช้งานทั้งหมดจะถูกเก็บไว้ออกจากพวกเขา

คุณจะบรรลุสิ่งนี้ได้อย่างไร โดยปกติแล้วโดยการแนะนำ abstractions เพิ่มเติม - ซึ่งอาจหรืออาจไม่สะท้อน abstractions ที่มีอยู่ ยกตัวอย่างเช่น GUI ของคุณไม่ได้จริงๆต้องรู้ว่าสิ่งที่ชนิดของรูปร่างมันคือการจัดการกับ เพียงแค่ต้องรู้ว่ามีพื้นที่บนหน้าจอที่ผู้ใช้สามารถแก้ไขรูปร่างได้

ดังนั้นคุณจึงกำหนดบทคัดย่อ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;

ไม่ว่าจะด้วยวิธีใดรหัสนอกตรรกะการสร้างนี้จะไม่ต้องจัดการกับรูปร่างที่เป็นรูปธรรม ในฐานะที่เป็นส่วนหนึ่งของการทำลายรูปร่างคุณจะต้องลบโรงงานออกอย่างชัดเจน แน่นอนตัวอย่างนี้ง่ายเกินไป แต่ฉันหวังว่าความคิดนั้นชัดเจน

การเลือกตัวเลือกที่เหมาะสม

ในแอพพลิเคชั่นที่ง่ายมากคุณอาจพบว่าวิธีการแก้ปัญหาที่สกปรก (การร่าย) ทำให้คุณได้รับผลตอบแทนสูงสุด

การคงรายการแยกต่างหากสำหรับแต่ละประเภทคอนกรีตอย่างชัดเจนอาจเป็นวิธีที่จะไปหากแอปพลิเคชันของคุณเกี่ยวข้องกับรูปร่างที่เป็นรูปธรรม แต่มีบางส่วนที่เป็นสากล ที่นี่มันทำให้รู้สึกเป็นนามธรรมเพียงเท่าที่ฟังก์ชั่นทั่วไปต้องใช้มัน

โดยทั่วไปแล้วการจ่ายเงินทั้งหมดจะเกิดขึ้นถ้าคุณมีตรรกะจำนวนมากที่ทำงานกับรูปร่างและรูปร่างที่แน่นอนนั้นเป็นรายละเอียดสำหรับแอปพลิเคชันของคุณ


ฉันชอบคำตอบของคุณคุณอธิบายปัญหาได้อย่างสมบูรณ์แบบ ปัญหาที่ฉันเผชิญไม่ใช่กับรูปร่าง แต่มีงานที่แตกต่างกันสำหรับเครื่องพิมพ์ 3D (เช่น: PrintPatternInZoneJob, TakePhotoOfZone, ฯลฯ ) โดยมี AbstractJob เป็นคลาสพื้นฐาน วิธีเสมือนคือ execute () และไม่ใช่ getPerimeter () ครั้งเดียวที่ฉันต้องใช้การใช้งานที่เป็นรูปธรรมคือการเติมข้อมูลเฉพาะที่งานต้องการ (รายการคะแนนตำแหน่งอุณหภูมิ ฯลฯ ) ด้วยวิดเจ็ตเฉพาะ ดูเหมือนว่าการแนบมุมมองไปยังแต่ละงานนั้นไม่ใช่สิ่งที่ต้องทำในกรณีนี้ แต่ฉันไม่เห็นวิธีการปรับวิสัยทัศน์ของคุณให้เข้ากับ pb ของฉัน
ElevenJune

หากคุณไม่ต้องการที่จะเก็บรายการแยกต่างหากคุณสามารถใช้ viewSelector มากกว่า viewFactory [rect, rectView]() { rectView.bind(rect); return rectView; }นี้: โดยวิธีการนี้แน่นอนควรจะทำในโมดูลการนำเสนอเช่นใน RectangleCreatedEventHandler
doubleYou

3
สิ่งนี้ถูกกล่าวว่าอย่าพยายามทำสิ่งนี้มากเกินไป ประโยชน์ของสิ่งที่เป็นนามธรรมจะยังคงมีค่าเกินกว่าค่าใช้จ่ายในการขนเพิ่มเติม บางครั้งนักแสดงที่วางไว้อย่างดีหรือตรรกะที่แยกต่างหากอาจจะดีกว่า
doubleYou

2

วิธีการหนึ่งที่จะทำให้สิ่งต่าง ๆ เป็นเรื่องธรรมดามากขึ้นเพื่อหลีกเลี่ยงการคัดเลือกเฉพาะประเภท

คุณสามารถนำ getter / setter พื้นฐานของคุณสมบัติ float " มิติ " ในคลาสพื้นฐานซึ่งตั้งค่าในแผนที่โดยยึดตามคีย์เฉพาะสำหรับชื่อคุณสมบัติ ตัวอย่างด้านล่าง:

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

จากนั้นในคลาสผู้จัดการของคุณคุณต้องใช้ฟังก์ชันเดียวเท่านั้นเช่นด้านล่าง:

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 เช่นกัน) คุณสามารถสร้างอินสแตนซ์มุมมองที่เหมาะสมเมื่อคุณสร้างคลาสรูปร่างที่เจาะจง เช่น:

  • ยกตัวอย่างสแควร์และ SquareEditView;
  • ส่งผ่านอินสแตนซ์สแควร์ไปยังวัตถุ SquareEditView
  • (เป็นทางเลือก) แทนที่จะมี ShapeManager ในมุมมองหลักของคุณคุณยังคงสามารถเก็บรายการรูปร่างได้
  • ภายใน SquareEditView คุณเก็บการอ้างอิงถึง Square สิ่งนี้จะช่วยลดความจำเป็นในการคัดเลือกเพื่อแก้ไขวัตถุ

ฉันชอบคำแนะนำแรกและคิดถึงมันอยู่แล้ว แต่มันค่อนข้าง จำกัด หากคุณต้องการเก็บตัวแปรต่าง ๆ (float, pointers, arrays) สำหรับข้อเสนอแนะที่สองถ้าสี่เหลี่ยมนั้นอินสแตนซ์แล้ว (ฉันคลิกบนมุมมองนั้น) ฉันจะรู้ได้อย่างไรว่ามันเป็นวัตถุสแควร์ * รายการการจัดเก็บรูปร่างกลับAbstractShape *
ElevenJune

@ElevenJune - ใช่ข้อเสนอแนะทั้งหมดมีข้อเสียของพวกเขา; สำหรับครั้งแรกที่คุณจะต้องใช้สิ่งที่ซับซ้อนมากกว่าแผนที่ง่าย ๆ ถ้าคุณต้องการคุณสมบัติประเภทอื่น ๆ ข้อเสนอแนะที่สองเปลี่ยนวิธีการจัดเก็บรูปร่าง; คุณจัดเก็บรูปร่างพื้นฐานในรายการ แต่ในเวลาเดียวกันคุณต้องให้การอ้างอิงรูปร่างที่เฉพาะเจาะจงกับมุมมอง บางทีคุณอาจให้รายละเอียดเพิ่มเติมเกี่ยวกับสถานการณ์ของคุณดังนั้นเราสามารถประเมินได้ว่าวิธีการเหล่านี้ดีกว่าการแสดง dynamic_cast
Emerson Cardoso

@ElevenJune - จุดทั้งหมดของการมีวัตถุมุมมองคือเพื่อให้ GUI ของคุณไม่จำเป็นต้องรู้ว่ามันจะทำงานร่วมกับประเภทของสแควร์ วัตถุมุมมองให้สิ่งที่จำเป็นในการ "ดู" วัตถุ (สิ่งที่คุณกำหนดว่าเป็น) และภายในรู้ว่ามันกำลังใช้อินสแตนซ์ของคลาสสแควร์ GUI โต้ตอบกับอินสแตนซ์ของ SquareView เท่านั้น ดังนั้นคุณไม่สามารถคลิกที่คลาส 'Square' คุณสามารถคลิกที่คลาส SquareView เท่านั้น การเปลี่ยนพารามิเตอร์ใน SquareView จะอัปเดตคลาส Square พื้นฐาน ....
Dunk

... วิธีการนี้ช่วยให้คุณกำจัดคลาส ShapeManager ของคุณได้เป็นอย่างดี นี่จะทำให้การออกแบบของคุณง่ายขึ้นอย่างแน่นอน ฉันมักจะพูดว่าถ้าคุณเรียกชั้นเรียนผู้จัดการก็ถือว่าเป็นการออกแบบที่ไม่ดีและคิดออกอย่างอื่น คลาสผู้จัดการไม่ดีด้วยเหตุผลมากมายโดยเฉพาะอย่างยิ่งปัญหาชั้นเรียนของพระเจ้าและความจริงที่ว่าไม่มีใครรู้ว่าสิ่งที่เรียนทำจริงสามารถทำได้และไม่สามารถทำได้เพราะผู้จัดการสามารถทำอะไรก็ได้แม้กระทั่งเกี่ยวข้องกับสิ่งที่พวกเขากำลังจัดการ คุณสามารถเดิมพันผู้พัฒนาเหล่านั้นที่ติดตามคุณจะได้รับประโยชน์จากสิ่งนั้นที่นำไปสู่ลูกใหญ่ก้อนโคลนทั่วไป
Dunk

1
... คุณประสบปัญหานี้แล้ว ทำไมบนโลกนี้จึงเหมาะสมที่ผู้จัดการจะต้องเป็นคนที่เปลี่ยนขนาดรูปร่าง ทำไมผู้จัดการจะคำนวณเส้นรอบวงของรูปร่าง ในกรณีที่คุณไม่เข้าใจฉันชอบ "ข้อเสนอแนะอื่น"
Dunk
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.