Thiết kế phù hợp để tránh sử dụng Dynamic_cast?


9

Sau khi thực hiện một số nghiên cứu, tôi dường như không thể tìm thấy một ví dụ đơn giản nào giải quyết vấn đề mà tôi gặp phải thường xuyên.

Giả sử tôi muốn tạo một ứng dụng nhỏ nơi tôi có thể tạo Squares, Circles và các hình dạng khác, hiển thị chúng trên màn hình, sửa đổi các thuộc tính của chúng sau khi chọn chúng, sau đó tính toán tất cả các chu vi của chúng.

Tôi sẽ làm lớp mô hình như thế này:

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

(Hãy tưởng tượng tôi có nhiều lớp hình dạng hơn: hình tam giác, hình lục giác, với mỗi lần biến proprers của chúng và các biểu đồ và setters liên quan.

Bây giờ tôi có một ShapeManager, khởi tạo và lưu trữ tất cả các hình dạng trong một mảng:

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

Cuối cùng, tôi có một khung nhìn với các hộp quay để thay đổi từng tham số cho từng loại hình dạng. Ví dụ: khi tôi chọn một hình vuông trên màn hình, tiện ích tham số chỉ hiển thị Squarecác tham số liên quan (nhờ AbstractShape::getType()) và đề xuất thay đổi độ rộng của hình vuông. Để làm điều đó tôi cần một chức năng cho phép tôi sửa đổi độ rộng trong ShapeManagervà đây là cách tôi thực hiện:

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

Có một thiết kế tốt hơn tránh cho tôi sử dụng dynamic_castvà để thực hiện một cặp getter / setter ShapeManagercho mỗi biến lớp con tôi có thể có không? Tôi đã thử sử dụng mẫu nhưng không thành công .


Vấn đề tôi đang phải đối mặt là không thực sự với Shapes nhưng với nhau Jobs cho một máy in 3D (ví dụ: PrintPatternInZoneJob, TakePhotoOfZone, vv) với AbstractJobnhư lớp cơ sở của họ. Phương pháp ảo là execute()và không getPerimeter(). Lần duy nhất tôi cần sử dụng cụ thể là điền thông tin cụ thể mà công việc cần :

  • PrintPatternInZone cần danh sách các điểm cần in, vị trí của vùng, một số thông số in như nhiệt độ

  • TakePhotoOfZone cần khu vực nào để chụp ảnh, đường dẫn ảnh sẽ được lưu, kích thước, v.v ...

Khi đó tôi sẽ gọi execute(), Jobs sẽ sử dụng thông tin cụ thể mà họ có để nhận ra hành động mà họ phải làm.

Lần duy nhất tôi cần sử dụng loại công việc cụ thể là khi tôi điền hoặc hiển thị thông tin luận án (nếu TakePhotoOfZone Jobđược chọn, một tiện ích hiển thị và sửa đổi các tham số vùng, đường dẫn và kích thước sẽ được hiển thị).

Các Jobs sau đó được đưa vào một danh sách các Jobcông việc đầu tiên, thực hiện nó (bằng cách gọi AbstractJob::execute()), tiếp theo, tiếp tục và tiếp tục cho đến khi kết thúc danh sách. (Đây là lý do tại sao tôi sử dụng thừa kế).

Để lưu trữ các loại tham số khác nhau, tôi sử dụng JsonObject:

  • lợi thế: cùng cấu trúc cho bất kỳ công việc nào, không có Dynamic_cast khi cài đặt hoặc đọc tham số

  • vấn đề: không thể lưu trữ con trỏ (đến Patternhoặc Zone)

Bạn có điều gì có một cách tốt hơn để lưu trữ dữ liệu?

Sau đó, làm thế nào bạn sẽ lưu trữ loại cụ thể của việcJob sử dụng nó khi tôi phải sửa đổi các tham số cụ thể của loại đó? JobManagerchỉ có một danh sách AbstractJob*.


5
Có vẻ như ShapeManager của bạn sẽ trở thành một lớp Thần, bởi vì về cơ bản nó sẽ chứa tất cả các phương thức setter cho tất cả các loại hình dạng.
Emerson Cardoso

Bạn đã xem xét một thiết kế "túi tài sản"? Chẳng hạn như changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)nơi PropertyKeycó thể là enum hoặc chuỗi và "Width" (biểu thị rằng lệnh gọi đến setter sẽ cập nhật giá trị của chiều rộng) là một trong những giá trị được phép.
rwong

Mặc dù một số túi tài sản được coi là chống mẫu OO, nhưng có những tình huống khi sử dụng túi tài sản đơn giản hóa thiết kế, trong đó mọi sự thay thế khác sẽ khiến mọi thứ trở nên phức tạp hơn. Mặc dù, để xác định xem túi thuộc tính có phù hợp với trường hợp sử dụng của bạn hay không, cần thêm thông tin (chẳng hạn như cách mã GUI tương tác với getter / setter).
rwong

Tôi đã xem xét thiết kế túi thuộc tính (mặc dù tôi không biết tên của nó) nhưng với một thùng chứa đối tượng JSON. Nó chắc chắn có thể hoạt động nhưng tôi nghĩ rằng nó không phải là một thiết kế thanh lịch và rằng một lựa chọn tốt hơn có thể tồn tại. Tại sao nó được coi là một mô hình chống OO?
ElevenJune

Ví dụ, nếu tôi muốn lưu trữ một con trỏ để sử dụng nó sau này, tôi phải làm thế nào?
ElevenJune

Câu trả lời:


10

Tôi muốn mở rộng trên "đề nghị khác" của Emerson Cardoso vì tôi tin rằng đó là cách tiếp cận chính xác trong trường hợp chung - mặc dù bạn có thể tìm thấy các giải pháp khác phù hợp hơn với bất kỳ vấn đề cụ thể nào.

Vấn đề

Trong ví dụ của bạn, AbstractShapelớp có một getType()phương thức cơ bản xác định loại cụ thể. Đây thường là một dấu hiệu cho thấy bạn không có một sự trừu tượng tốt. Rốt cuộc, toàn bộ vấn đề trừu tượng hóa, không phải quan tâm đến các chi tiết của loại bê tông.

Ngoài ra, trong trường hợp bạn không quen thuộc với nó, bạn nên đọc Nguyên tắc mở / đóng. Nó thường được giải thích với một ví dụ về hình dạng, vì vậy bạn sẽ cảm thấy như ở nhà.

Tóm tắt hữu ích

Tôi giả sử bạn đã giới thiệu AbstractShapebởi vì bạn thấy nó hữu ích cho một cái gì đó. Nhiều khả năng, một số phần trong ứng dụng của bạn cần biết chu vi của các hình dạng, bất kể hình dạng là gì.

Đây là nơi trừu tượng có ý nghĩa. Bởi vì mô-đun này không liên quan đến chính nó với hình dạng cụ thể, nó chỉ có thể phụ thuộc vào AbstractShape. Vì lý do tương tự, nó không cần getType()phương thức - vì vậy bạn nên loại bỏ nó.

Các phần khác của ứng dụng sẽ chỉ hoạt động với một loại hình dạng cụ thể, ví dụ Rectangle. Những khu vực đó sẽ không được hưởng lợi từ một AbstractShapelớp học, vì vậy bạn không nên sử dụng nó ở đó. Để chỉ truyền hình dạng chính xác cho các bộ phận này, bạn cần lưu trữ các hình dạng bê tông riêng biệt. (Bạn có thể lưu trữ chúng dưới dạng AbstractShapebổ sung hoặc kết hợp chúng khi đang bay).

Giảm thiểu sử dụng bê tông

Không có cách nào xung quanh nó: bạn cần các loại bê tông ở một số nơi - ít nhất là trong quá trình xây dựng. Tuy nhiên, đôi khi tốt nhất là giữ việc sử dụng các loại bê tông giới hạn trong một số khu vực được xác định rõ. Các khu vực riêng biệt này có mục đích duy nhất là xử lý các loại khác nhau - trong khi tất cả logic ứng dụng được loại bỏ khỏi chúng.

Làm thế nào để bạn đạt được điều này? Thông thường, bằng cách giới thiệu nhiều trừu tượng hơn - có thể hoặc không thể phản ánh các trừu tượng hiện có. Ví dụ, GUI của bạn không thực sự cần biết loại hình đó đang xử lý. Nó chỉ cần biết rằng có một khu vực trên màn hình nơi người dùng có thể chỉnh sửa một hình dạng.

Vì vậy, bạn xác định một bản tóm tắt ShapeEditViewmà bạn có RectangleEditViewvà các CircleEditViewtriển khai chứa các hộp văn bản thực tế cho chiều rộng / chiều cao hoặc bán kính.

Trong bước đầu tiên, bạn có thể tạo RectangleEditViewbất cứ khi nào bạn tạo Rectanglevà sau đó đặt nó vào std::map<AbstractShape*, AbstractShapeView*>. Nếu bạn muốn tạo các chế độ xem khi bạn cần, thay vào đó, bạn có thể thực hiện các thao tác sau:

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

Dù bằng cách nào, mã bên ngoài logic sáng tạo này sẽ không phải xử lý các hình dạng cụ thể. Là một phần của sự phá hủy của một hình dạng, rõ ràng bạn cần phải loại bỏ nhà máy. Tất nhiên, ví dụ này được đơn giản hóa quá mức, nhưng tôi hy vọng ý tưởng này rõ ràng.

Lựa chọn phương án phù hợp

Trong các ứng dụng rất đơn giản, bạn có thể thấy rằng một giải pháp (đúc) bẩn chỉ mang lại cho bạn nhiều lợi ích nhất.

Hoàn toàn duy trì danh sách riêng cho từng loại bê tông có lẽ là cách tốt nhất nếu ứng dụng của bạn chủ yếu xử lý các hình dạng cụ thể, nhưng có một số phần là phổ quát. Ở đây, nó có ý nghĩa để trừu tượng chỉ cho đến khi các chức năng phổ biến yêu cầu nó.

Đi tất cả các cách thường trả tiền nếu bạn có nhiều logic hoạt động trên các hình dạng, và loại hình chính xác thực sự là một chi tiết cho ứng dụng của bạn.


Tôi thực sự thích câu trả lời của bạn, bạn đã mô tả hoàn hảo vấn đề. Vấn đề tôi gặp phải không thực sự xảy ra với Shapes mà là với các Công việc khác nhau dành cho máy in 3D (ví dụ: PrintPotypeInZoneJob, TakePhotoOfZone, v.v.) với AbstractJob là lớp cơ sở của họ. Phương thức ảo được thực thi () chứ không phải getPerimet (). Lần duy nhất tôi cần sử dụng cụ thể là điền thông tin cụ thể mà công việc cần (danh sách các điểm, vị trí, nhiệt độ, v.v.) bằng một vật dụng cụ thể. Gắn một quan điểm cho mỗi công việc dường như không phải là điều cần làm trong trường hợp cụ thể này nhưng tôi không thấy cách điều chỉnh tầm nhìn của bạn với pb của tôi.
ElevenJune

Nếu bạn không muốn giữ các danh sách riêng biệt, bạn có thể sử dụng viewSelector thay vì viewFactory : [rect, rectView]() { rectView.bind(rect); return rectView; }. Nhân tiện, điều này tất nhiên phải được thực hiện trong mô-đun trình bày, ví dụ như trong một hình chữ nhậtCreatedEventHandler.
doubleYou

3
Điều này đang được nói, cố gắng không quá kỹ sư này. Lợi ích của sự trừu tượng vẫn phải lớn hơn chi phí của việc trồng thêm. Đôi khi một diễn viên được đặt tốt, hoặc logic riêng biệt có thể được ưa thích hơn.
doubleYou

2

Một cách tiếp cận sẽ là làm cho công cụ tổng quát hơn để tránh truyền tới các loại cụ thể .

Bạn có thể triển khai một getter / setter cơ bản của các thuộc tính float "thứ nguyên " trong lớp cơ sở, nó đặt một giá trị trong bản đồ, dựa trên một khóa cụ thể cho tên thuộc tính. Ví dụ dưới đây:

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

Sau đó, trong lớp trình quản lý của bạn, bạn chỉ cần thực hiện một chức năng, như dưới đây:

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

Ví dụ về việc sử dụng trong Chế độ xem:

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

Một đề nghị khác:

Vì người quản lý của bạn chỉ hiển thị trình thiết lập và tính toán chu vi (cũng được hiển thị bởi Shape), bạn chỉ có thể khởi tạo một Chế độ xem phù hợp khi bạn khởi tạo một lớp Shape cụ thể. VÍ DỤ:

  • Khởi tạo hình vuông và hình vuông;
  • Truyền đối tượng Square cho đối tượng SquareEditView;
  • (tùy chọn) Thay vì có ShapeManager, trong chế độ xem chính của bạn, bạn vẫn có thể giữ một danh sách Hình dạng;
  • Trong SquareEditView, bạn giữ một tham chiếu đến Square; điều này sẽ loại bỏ sự cần thiết của việc chỉnh sửa các đối tượng.

Tôi thích đề xuất đầu tiên và đã nghĩ về nó, nhưng nó khá hạn chế nếu bạn muốn lưu trữ các biến khác nhau (float, con trỏ, mảng). Đối với gợi ý thứ hai, nếu hình vuông đã được khởi tạo (tôi đã nhấn vào nó trên khung nhìn) làm sao tôi biết đó là một đối tượng Square * ? danh sách lưu trữ các hình dạng trả về một AbstractShape * .
ElevenJune

@ElevenJune - vâng tất cả các đề xuất đều có nhược điểm của chúng; Đầu tiên, bạn sẽ cần triển khai một cái gì đó phức tạp hơn là một bản đồ đơn giản nếu bạn muốn có nhiều loại thuộc tính hơn. Gợi ý thứ hai thay đổi cách bạn lưu trữ các hình dạng; bạn lưu trữ hình dạng cơ sở trong danh sách, nhưng đồng thời bạn cần cung cấp tham chiếu hình dạng cụ thể cho Chế độ xem. Có lẽ bạn có thể cung cấp thêm chi tiết về kịch bản của mình, vì vậy chúng tôi có thể đánh giá xem các phương pháp này có tốt hơn chỉ đơn giản là thực hiện một Dynamic_cast.
Emerson Cardoso

@ElevenJune - toàn bộ quan điểm của việc có đối tượng xem là vì vậy GUI của bạn không cần biết nó đang hoạt động với một lớp kiểu Square. Đối tượng khung nhìn cung cấp những gì cần thiết để "xem" đối tượng (bất cứ điều gì bạn xác định đó là) và bên trong nó biết rằng nó đang sử dụng một thể hiện của lớp Square. GUI chỉ tương tác với phiên bản SquareView. Do đó, bạn không thể nhấp vào lớp 'Square'. Bạn chỉ có thể nhấp vào một lớp SquareView. Thay đổi tham số trên SquareView sẽ cập nhật lớp Square bên dưới ....
Dunk

... Cách tiếp cận này rất có thể cho phép bạn thoát khỏi lớp ShapeManager. Điều này gần như chắc chắn sẽ đơn giản hóa thiết kế của bạn. Tôi luôn nói rằng nếu bạn gọi một lớp là Manager thì hãy cho rằng đó là một thiết kế tồi và tìm ra thứ gì đó khác. Các lớp quản lý rất tệ vì vô số lý do, đáng chú ý nhất là vấn đề của lớp thần và thực tế là không ai biết lớp thực sự làm gì, có thể làm gì và không thể làm được vì Người quản lý có thể làm bất cứ điều gì thậm chí liên quan đến bất cứ điều gì họ đang quản lý. Bạn có thể đặt cược cho những nhà phát triển theo dõi bạn sẽ tận dụng lợi thế dẫn đến quả bóng lớn điển hình.
Dunk

1
... Bạn đã gặp phải vấn đề đó. Tại sao trên trái đất lại có ý nghĩa đối với người quản lý là người thay đổi kích thước của hình dạng? Tại sao người quản lý sẽ tính chu vi của một hình dạng? Trong trường hợp bạn không tìm ra nó, tôi thích "Một gợi ý khác".
Dunk
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.