Desain yang tepat untuk menghindari penggunaan dynamic_cast?

9

Setelah melakukan beberapa penelitian, saya tidak dapat menemukan contoh sederhana untuk menyelesaikan masalah yang sering saya temui.

Katakanlah saya ingin membuat aplikasi kecil di mana saya dapat membuat Squares, Circles, dan bentuk lainnya, menampilkannya di layar, memodifikasi properti mereka setelah memilihnya, dan kemudian menghitung semua perimeter mereka.

Saya akan melakukan kelas model seperti ini:

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

(Bayangkan saya memiliki lebih banyak kelas bentuk: segitiga, segi enam, dengan setiap kali variabel penekan mereka dan getter dan setter terkait. Masalah yang saya hadapi memiliki 8 subclass tetapi demi contoh saya berhenti di 2)

Sekarang saya punya ShapeManager, instantiating dan menyimpan semua bentuk dalam array:

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

Akhirnya, saya memiliki pandangan dengan spinbox untuk mengubah setiap parameter untuk setiap jenis bentuk. Sebagai contoh, ketika saya memilih kotak di layar, widget parameter hanya menampilkan Squareparameter terkait (terima kasih AbstractShape::getType()) dan mengusulkan untuk mengubah lebar kotak. Untuk melakukan itu saya memerlukan fungsi yang memungkinkan saya untuk memodifikasi lebar ShapeManager, dan ini adalah bagaimana saya melakukannya:

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

Apakah ada desain yang lebih baik yang menghindari saya untuk menggunakan dynamic_castdan mengimplementasikan pasangan pengambil / penyetel ShapeManageruntuk setiap variabel subkelas yang mungkin saya miliki? Saya sudah mencoba menggunakan template tetapi gagal .


Masalah yang dihadapi saya adalah tidak benar-benar dengan Shapes tetapi dengan berbeda Jobs untuk printer 3D (ex: PrintPatternInZoneJob, TakePhotoOfZone, dll) dengan AbstractJobsebagai kelas dasar mereka. Metode virtual adalah execute()dan tidak getPerimeter(). Satu-satunya waktu saya perlu menggunakan penggunaan konkret adalah mengisi informasi spesifik yang dibutuhkan pekerjaan :

  • PrintPatternInZone perlu daftar titik untuk dicetak, posisi zona, beberapa parameter pencetakan seperti suhu

  • TakePhotoOfZone membutuhkan zona apa untuk mengambil foto, jalur di mana foto akan disimpan, dimensi, dll ...

Ketika saya akan menelepon execute(), Jobs akan menggunakan informasi spesifik yang mereka miliki untuk menyadari tindakan yang seharusnya mereka lakukan.

Satu-satunya waktu saya perlu menggunakan jenis konkret Pekerjaan adalah ketika saya mengisi atau menampilkan informasi tesis ini (jika a TakePhotoOfZone Jobdipilih, widget yang menampilkan dan memodifikasi parameter zona, jalur, dan dimensi akan ditampilkan).

The Jobs kemudian dimasukkan ke dalam daftar Jobs yang mengambil pekerjaan pertama, mengeksekusinya (dengan memanggil AbstractJob::execute()), pergi ke yang berikutnya, dan terus sampai akhir daftar. (Inilah sebabnya saya menggunakan warisan).

Untuk menyimpan berbagai jenis parameter, saya menggunakan JsonObject:

  • Keuntungan: struktur yang sama untuk pekerjaan apa pun, tidak ada dynamic_cast saat mengatur atau membaca parameter

  • masalah: tidak dapat menyimpan pointer (ke Patternatau Zone)

Apakah Anda ada cara yang lebih baik untuk menyimpan data?

Lalu bagaimana Anda menyimpan tipe konkritJob untuk menggunakannya ketika saya harus memodifikasi parameter spesifik dari tipe itu? JobManagerhanya memiliki daftar AbstractJob*.

ElevenJune
sumber
5
Sepertinya ShapeManager Anda akan menjadi kelas Dewa, karena pada dasarnya akan berisi semua metode penyetel untuk semua jenis bentuk.
Emerson Cardoso
Sudahkah Anda mempertimbangkan desain "tas properti"? Seperti di changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)mana PropertyKeybisa menjadi enum atau string, dan "Lebar" (yang menandakan bahwa panggilan ke setter akan memperbarui nilai lebar) adalah salah satu dari nilai yang diizinkan.
rwong
Meskipun tas properti dianggap anti-pola OO oleh beberapa orang, ada situasi ketika menggunakan tas properti menyederhanakan desain, di mana setiap alternatif lain akan membuat segalanya lebih rumit. Padahal, untuk menentukan apakah tas properti cocok untuk kasus penggunaan Anda, diperlukan lebih banyak informasi (seperti bagaimana kode GUI berinteraksi dengan pengambil / penyetel).
rwong
Saya mempertimbangkan desain tas properti (meskipun saya tidak tahu namanya) tetapi dengan wadah objek JSON. Tentu bisa bekerja tetapi saya pikir itu bukan desain yang elegan dan bahwa pilihan yang lebih baik mungkin ada. Mengapa itu dianggap sebagai anti-pola OO?
ElevenJune
Misalnya, jika saya ingin menyimpan pointer untuk menggunakannya nanti, bagaimana saya melakukannya?
ElevenJuni

Jawaban:

10

Saya ingin memperluas "saran lain" Emerson Cardoso karena saya percaya itu adalah pendekatan yang benar dalam kasus umum - meskipun Anda tentu saja dapat menemukan solusi lain yang lebih cocok untuk masalah tertentu.

Masalah

Dalam contoh Anda, AbstractShapekelas memiliki getType()metode yang pada dasarnya mengidentifikasi tipe beton. Ini umumnya merupakan pertanda bahwa Anda tidak memiliki abstraksi yang baik. Inti dari abstraksi, tidak harus peduli dengan detail dari tipe konkret.

Juga, jika Anda tidak terbiasa dengan itu, Anda harus membaca tentang Prinsip Terbuka / Tertutup. Ini sering dijelaskan dengan contoh bentuk, sehingga Anda akan merasa seperti di rumah.

Abstraksi yang Berguna

Saya berasumsi Anda telah memperkenalkannya AbstractShapekarena Anda merasa itu berguna untuk sesuatu. Kemungkinan besar, beberapa bagian dari aplikasi Anda perlu mengetahui perimeter bentuk, terlepas dari apa bentuknya.

Ini adalah tempat di mana abstraksi masuk akal. Karena modul ini tidak mementingkan bentuk beton, modul ini AbstractShapehanya dapat bergantung pada modul . Untuk alasan yang sama, tidak perlu getType()metode - jadi Anda harus menyingkirkannya.

Bagian lain dari aplikasi hanya akan berfungsi dengan bentuk tertentu, misalnya Rectangle. Area-area itu tidak akan mendapat manfaat dari AbstractShapekelas, jadi Anda tidak boleh menggunakannya di sana. Untuk hanya memberikan bentuk yang benar ke bagian-bagian ini, Anda perlu menyimpan bentuk beton secara terpisah. (Anda dapat menyimpannya sebagai AbstractShapetambahan, atau menggabungkannya dengan cepat).

Meminimalkan Penggunaan Beton

Tidak ada jalan lain: Anda perlu jenis beton di beberapa tempat - setidaknya selama konstruksi. Namun, kadang-kadang yang terbaik untuk menjaga penggunaan jenis beton terbatas pada beberapa area yang didefinisikan dengan baik. Area terpisah ini memiliki satu-satunya tujuan berurusan dengan berbagai jenis - sementara semua logika aplikasi tidak digunakan.

Bagaimana Anda mencapai ini? Biasanya, dengan memperkenalkan lebih banyak abstraksi - yang mungkin atau mungkin tidak mencerminkan abstraksi yang ada. Misalnya, GUI Anda tidak benar - benar perlu tahu bentuk apa yang dihadapinya. Hanya perlu tahu bahwa ada area di layar tempat pengguna dapat mengedit bentuk.

Jadi, Anda mendefinisikan abstrak ShapeEditViewyang Anda miliki RectangleEditViewdan CircleEditViewimplementasi yang menampung kotak teks aktual untuk lebar / tinggi atau radius.

Pada langkah pertama, Anda bisa membuat RectangleEditViewkapan saja Anda membuat Rectangledan kemudian memasukkannya ke dalam std::map<AbstractShape*, AbstractShapeView*>. Jika Anda lebih suka membuat tampilan sesuai kebutuhan, Anda dapat melakukan yang berikut:

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

Either way, kode di luar logika penciptaan ini tidak harus berurusan dengan bentuk konkret. Sebagai bagian dari penghancuran bentuk, Anda harus menghapus pabrik, jelas. Tentu saja, contoh ini terlalu disederhanakan, tapi saya harap idenya jelas.

Memilih Opsi yang tepat

Dalam aplikasi yang sangat sederhana, Anda mungkin menemukan bahwa solusi (casting) kotor hanya memberi Anda hasil maksimal.

Mempertahankan daftar terpisah secara eksplisit untuk setiap jenis beton mungkin merupakan cara yang harus dilakukan jika aplikasi Anda terutama berkaitan dengan bentuk beton, tetapi memiliki beberapa bagian yang bersifat universal. Di sini, masuk akal untuk abstrak sejauh fungsi umum membutuhkannya.

Secara keseluruhan, membayar jika Anda memiliki banyak logika yang beroperasi pada bentuk, dan bentuk yang tepat benar-benar detail untuk aplikasi Anda.

dua kali kamu
sumber
Saya sangat suka jawaban Anda, Anda menggambarkan masalahnya dengan sempurna. Masalah yang saya hadapi sebenarnya bukan dengan Shapes tetapi dengan Pekerjaan berbeda untuk printer 3D (mis: PrintPatternInZoneJob, TakePhotoOfZone, dll.) Dengan AbstractJob sebagai kelas dasar mereka. Metode virtual mengeksekusi () dan bukan getPerimeter (). Satu-satunya waktu saya perlu menggunakan penggunaan konkret adalah mengisi informasi spesifik yang dibutuhkan suatu pekerjaan (daftar poin, posisi, suhu, dll.) Dengan widget tertentu. Melampirkan pandangan ke setiap pekerjaan tampaknya bukan hal yang harus dilakukan dalam kasus khusus ini tetapi saya tidak melihat bagaimana menyesuaikan visi Anda dengan pb saya.
ElevenJune
Jika Anda tidak ingin menyimpan daftar terpisah, Anda dapat menggunakan viewSelector daripada viewFactory: [rect, rectView]() { rectView.bind(rect); return rectView; }. Omong-omong, ini tentu saja harus dilakukan dalam modul presentasi, misalnya dalam RectangleCreatedEventHandler.
gandakan kamu
3
Ini dikatakan, cobalah untuk tidak merekayasa hal ini. Manfaat dari abstraksi ini masih harus lebih besar daripada biaya dari tambahan bulu. Kadang-kadang pemain yang ditempatkan dengan baik, atau logika terpisah mungkin lebih disukai.
gandakan kamu
2

Salah satu pendekatan adalah membuat barang lebih umum untuk menghindari casting ke tipe tertentu .

Anda bisa menerapkan pengambil / penyetel dasar properti float " dimensi " di kelas dasar, yang menetapkan nilai dalam peta, berdasarkan pada kunci spesifik untuk nama properti. Contoh di bawah ini:

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

Kemudian, di kelas manajer Anda, Anda hanya perlu menerapkan satu fungsi, seperti di bawah ini:

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

Contoh penggunaan dalam Tampilan:

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

Saran lain:

Karena manajer Anda hanya mengekspos setter dan perhitungan perimeter (yang diekspos oleh Shape juga), Anda bisa membuat Instantiate tampilan yang tepat ketika Anda instantiate kelas Shape tertentu. MISALNYA:

  • Instantiate a Square dan SquareEditView;
  • Berikan contoh Square ke objek SquareEditView;
  • (opsional) Alih-alih memiliki ShapeManager, di tampilan utama Anda, Anda masih bisa menyimpan daftar Bentuk;
  • Di dalam SquareEditView, Anda menyimpan referensi ke Kotak; ini akan menghilangkan kebutuhan casting untuk mengedit objek.
Emerson Cardoso
sumber
Saya suka saran pertama dan sudah memikirkannya, tetapi cukup membatasi jika Anda ingin menyimpan variabel yang berbeda (float, pointer, array). Untuk saran kedua, jika persegi sudah instantiated (saya klik di atasnya pada tampilan) bagaimana saya tahu itu objek Square * ? daftar yang menyimpan bentuk mengembalikan AbstractShape * .
ElevenJuni
@ElevenJune - ya semua saran memiliki kekurangannya; untuk yang pertama Anda perlu mengimplementasikan sesuatu yang lebih kompleks daripada peta sederhana jika Anda menginginkan lebih banyak jenis properti. Saran kedua mengubah cara Anda menyimpan bentuk; Anda menyimpan bentuk dasar dalam daftar, tetapi pada saat yang sama Anda harus memberikan referensi bentuk spesifik ke tampilan. Mungkin Anda bisa memberikan detail lebih banyak tentang skenario Anda, jadi kami dapat mengevaluasi apakah pendekatan ini lebih baik daripada hanya melakukan dynamic_cast.
Emerson Cardoso
@ElevenJune - inti dari memiliki objek tampilan adalah agar GUI Anda tidak perlu tahu bahwa ia bekerja dengan kelas tipe Square. Obyek tampilan menyediakan apa yang diperlukan untuk "melihat" objek (apa pun yang Anda tetapkan itu) dan secara internal ia tahu bahwa objek itu menggunakan instance dari kelas Square. GUI hanya berinteraksi dengan instance SquareView. Dengan demikian, Anda tidak dapat mengklik kelas 'Kotak'. Anda hanya bisa mengklik pada kelas SquareView. Mengubah parameter pada SquareView akan memperbarui kelas Square yang mendasarinya ....
Dunk
... Pendekatan ini bisa sangat baik membiarkan Anda menyingkirkan kelas ShapeManager Anda. Ini hampir pasti akan menyederhanakan desain Anda. Saya selalu mengatakan jika Anda menyebut kelas sebagai Manajer, maka anggap itu desain yang buruk dan cari tahu yang lain. Kelas manajer buruk karena berbagai alasan, terutama masalah kelas dewa dan fakta bahwa tidak ada yang tahu apa yang sebenarnya dilakukan kelas, dapat dilakukan dan tidak dapat dilakukan karena Manajer dapat melakukan apa pun yang berhubungan dengan apa pun yang mereka kelola. Anda bisa bertaruh para pengembang yang mengikuti Anda akan mengambil keuntungan dari yang mengarah ke bola-besar-lumpur-khas.
Dunk
1
... Anda sudah mengalami masalah itu. Mengapa seorang manajer bisa masuk akal untuk mengubah dimensi bentuk? Mengapa manajer menghitung perimeter bentuk? Jika Anda tidak mengetahuinya, saya suka "Saran lain".
Dunk