Dynamic_cast kullanımını önlemek için uygun tasarım?


9

Bazı araştırmalar yaptıktan sonra sık karşılaştığım bir sorunu çözen basit bir örnek bulamıyorum.

Diyelim ki Square, s, Circles ve diğer şekilleri oluşturabildiğim, bir ekranda görüntüleyebildiğim, seçtikten sonra özelliklerini değiştirebildiğim ve ardından tüm çevrelerini hesaplayabildiğim küçük bir uygulama oluşturmak istiyorum .

Model sınıfını şöyle yaparım:

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

(Daha fazla şekil sınıfım olduğunu düşünün: üçgenler, altıgenler, her seferinde proprers değişkenleri ve ilişkili alıcılar ve ayarlayıcılar. Karşılaştığım problemlerin 8 alt sınıfı vardı ama 2'de durduğum örnek için)

Şimdi ShapeManagerbir dizi, tüm şekiller bir dizi depolama ve depolama var:

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

Son olarak, her şekil türü için her parametreyi değiştirmek için spinboxes ile bir görünüm var. Örneğin, ekranda bir kare seçtiğimde, parametre widget'ı yalnızca Squareilgili parametreleri (sayesinde AbstractShape::getType()) görüntüler ve karenin genişliğini değiştirmeyi önerir. Bunu yapmak için bana genişliği değiştirmek için izin veren bir işleve ihtiyacım var ShapeManagerve ben bunu nasıl yaparım:

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

Sahip olabileceğim her alt sınıf değişkeni için dynamic_castalıcı / ayarlayıcı çiftini kullanmamı ve uygulamamı ShapeManagerengelleyen daha iyi bir tasarım var mı? Zaten şablonu kullanmaya çalıştım ama başarısız oldum .


Yüzleştiğim sorun Şekiller ile ancak gerçekten değil farklı Jobs : 3D yazıcı için (örn PrintPatternInZoneJob, TakePhotoOfZoneile, vs.) AbstractJobonların temel sınıf olarak. Sanal yöntem execute()değil getPerimeter(). Somut kullanımı kullanmam gereken tek zaman, bir işin ihtiyaç duyduğu belirli bilgileri doldurmaktır :

  • PrintPatternInZone yazdırmak için nokta listesine, bölgenin konumuna, sıcaklık gibi bazı baskı parametrelerine ihtiyaç duyar

  • TakePhotoOfZone fotoğrafın hangi bölgeye, fotoğrafın kaydedileceği yola, boyutlara vb. ihtiyaç duyar.

Daha sonra arayacağımda execute(), İşler yapmaları gereken eylemi gerçekleştirmek için ihtiyaç duydukları bilgileri kullanacaklar.

Bir İşin somut tipini kullanmam gereken tek zaman, bu bilgileri doldururken veya görüntülerken (a TakePhotoOfZone Jobseçilirse, bölge, yol ve boyut parametrelerini görüntüleyen ve değiştiren bir widget gösterilecektir).

Daha Jobsonra Jobs, ilk işi alan, yürüten (çağırarak AbstractJob::execute()), bir sonrakine gider, listenin sonuna kadar devam eden s listesine eklenir . (Bu yüzden miras kullanıyorum).

Farklı parametre türlerini saklamak için aşağıdakileri kullanıyorum JsonObject:

  • avantajları: herhangi bir iş için aynı yapı, parametreleri ayarlarken veya okurken dynamic_cast yok

  • Sorun: İşaretçileri saklayamıyor ( Patternveya veya Zone)

Veri depolamanın daha iyi bir yolu var mı?

O zaman bu tipin belirli parametrelerini değiştirmek zorunda kaldığım beton tipini nasıl saklarsınızJob ? JobManagersadece bir listesi var AbstractJob*.


5
ShapeManager'ınız bir Tanrı sınıfı olacak gibi görünüyor, çünkü temel olarak tüm şekil türleri için tüm ayarlayıcı yöntemleri içerecektir.
Emerson Cardoso

Bir "mülkiyet çantası" tasarımı düşündünüz mü? Bu gibi changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)durumlarda PropertyKeybir enum ne de bir tel ve izin verilen değerler arasında yer almaktadır (ayarlayıcı çağrı genişliği değerini günceller anlamına gelir) "En" olabilir.
rwong

Bazıları tarafından özellik çantası OO anti-desen olarak kabul edilmesine rağmen, özellik çantasını kullanmanın tasarımı basitleştirdiği durumlar vardır, diğer her alternatifin işleri daha karmaşık hale getirecektir. Bununla birlikte, özellik çantasının kullanım durumunuz için uygun olup olmadığını belirlemek için daha fazla bilgiye ihtiyaç vardır (GUI kodunun alıcı / ayarlayıcı ile nasıl etkileşime gireceği gibi).
rwong

(Ben adını bilmiyordum rağmen) özellik çanta tasarımı düşündüm ama bir JSON nesne konteyner ile. Elbette işe yarayabilir ama zarif bir tasarım olmadığını ve daha iyi bir seçeneğin var olabileceğini düşündüm. Neden bir OO anti-paterni olarak kabul edilir?
ElevenHaziran

Örneğin, daha sonra kullanmak üzere bir işaretçiyi saklamak istersem, nasıl yaparım?
ElevenJune

Yanıtlar:


10

Emerson Cardoso'nun "diğer önerisi" ni genişletmek istiyorum, çünkü genel durum için doğru yaklaşım olduğuna inanıyorum - tabii ki herhangi bir soruna daha uygun başka çözümler de bulabilirsiniz.

Sorun

Örneğin, AbstractShapesınıfta getType()temel olarak somut türü tanımlayan bir yöntem vardır. Bu genellikle iyi bir soyutlamaya sahip olmadığınızın bir işaretidir. Sonuçta, soyutlamanın tüm noktası, beton tipinin ayrıntılarını önemsemek zorunda değildir.

Ayrıca, aşina değilseniz, Açık / Kapalı Prensibi'ni okumalısınız. Genellikle bir şekil örneği ile açıklanır, böylece kendinizi evinizde gibi hissedersiniz.

Yararlı Soyutlamalar

Sanırım AbstractShapebunu bir şey için yararlı bulduğunuz için tanıttınız. Büyük olasılıkla, uygulamanızın bir kısmının, şeklin ne olduğuna bakılmaksızın şekillerin çevresini bilmesi gerekir.

Burası soyutlamanın mantıklı olduğu yerdir. Bu modül kendisini somut şekillerle ilgilendirmediğinden, AbstractShapesadece buna bağlı olabilir . Aynı nedenden dolayı, getType()yönteme ihtiyaç duymaz - bu yüzden ondan kurtulmalısınız.

Uygulamanın diğer kısımları sadece belirli bir şekil ile çalışır, örn Rectangle. Bu alanlar bir AbstractShapesınıftan yararlanamayacaktır , bu yüzden orada kullanmamalısınız. Bu parçalara sadece doğru şekli geçirmek için, beton şekilleri ayrı ayrı saklamanız gerekir. (Bunları AbstractShapeek olarak saklayabilir veya anında birleştirebilirsiniz).

Beton Kullanımını En Aza İndirme

Etrafında bir yol yok: bazı yerlerde beton tiplerine ihtiyacınız var - en azından inşaat sırasında. Bununla birlikte, bazen beton tiplerinin kullanımını birkaç iyi tanımlanmış alanla sınırlı tutmak en iyisidir. Bu ayrı alanların tek amacı farklı türlerle uğraşmaktır - tüm uygulama mantığı bunlardan uzak tutulur.

Bunu nasıl başarıyorsunuz? Genellikle, daha fazla soyutlama getirerek - mevcut soyutlamaları yansıtabilir veya yansıtmayabilir. Örneğin, GUI'nizin ne tür bir şekil ile uğraştığını gerçekten bilmesine gerek yoktur . Ekranda, kullanıcının bir şekli düzenleyebileceği bir alan olduğunu bilmek yeterlidir.

Bir özet tanımlamak Böylece ShapeEditViewhangi sahip RectangleEditViewve CircleEditViewgenişlik / yükseklik veya yarıçap fiili metin kutuları tutmak uygulamalar.

İlk adımda, bir oluşturduğunuzda bir RectangleEditViewzaman oluşturabilir Rectangleve ardından bir std::map<AbstractShape*, AbstractShapeView*>. Görünümleri istediğiniz gibi oluşturmayı tercih ederseniz, aşağıdakileri yapabilirsiniz:

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

Her iki durumda da, bu yaratma mantığının dışındaki kodun somut şekillerle uğraşması gerekmeyecektir. Bir şeklin tahrip edilmesinin bir parçası olarak, açıkça fabrikayı kaldırmanız gerekir. Tabii ki, bu örnek aşırı basitleştirilmiş, ancak umarım fikir açıktır.

Doğru Seçeneği Seçme

Çok basit uygulamalarda, kirli (döküm) bir çözümün paranızın karşılığını en iyi şekilde verdiğini görebilirsiniz.

Her somut tür için ayrı ayrı listelerin açıkça tutulması, uygulamanız esas olarak somut şekillerle ilgileniyor, ancak evrensel olan bazı parçalara sahipse, muhtemelen gidilecek yoldur. Burada, sadece ortak işlevsellik gerektirdiği sürece soyutlamak mantıklıdır.

Şekiller üzerinde çalışan çok fazla mantığınız varsa ve yolun şekli gerçekten uygulamanız için bir ayrıntıysa, tüm yol boyunca gitmek genellikle ödeme yapar.


Cevabınızı gerçekten çok beğendim, sorunu mükemmel bir şekilde tanımladınız. Karşılaştığım sorun aslında Şekillerle değil, temel sınıf olarak AbstractJob ile bir 3D yazıcı için farklı İşlerle (ör: PrintPatternInZoneJob, TakePhotoOfZone, vb.). Sanal yöntem, getPerimeter () değil, execute () yöntemidir. Somut kullanımı kullanmam gereken tek zaman, bir işin ihtiyaç duyduğu belirli bilgileri (nokta listesi, pozisyon, sıcaklık vb.) Belirli bir widget ile doldurmaktır. Her işe bir görüş eklemek, bu özel durumda yapılacak bir şey gibi görünmüyor, ancak vizyonunuzu pb'ye nasıl uyarlayacağımı göremiyorum.
ElevenHaziran

Ayrı listeler tutmak istemiyorsanız, viewFactory: yerine viewSelector kullanabilirsiniz [rect, rectView]() { rectView.bind(rect); return rectView; }. Bu arada, bu tabii ki sunum modülünde, örneğin RectangleCreatedEventHandler'da yapılmalıdır.
Doubleyou

3
Bu söyleniyor, fazla mühendislik yapmamaya çalışın. Soyutlamanın yararı, hala ek tesisatın maliyetinden daha ağır basmalıdır. Bazen iyi yerleştirilmiş bir döküm veya ayrı bir mantık tercih edilebilir.
Doubleyou

2

Bir yaklaşım, belirli türlere döküm yapılmasını önlemek için işleri daha genel hale getirmektir .

Temel sınıfta, özellik adı için belirli bir anahtara dayalı olarak haritadaki bir değeri ayarlayan " boyut " kayan nokta özelliklerinin temel alıcısını / ayarlayıcısını uygulayabilirsiniz . Aşağıdaki örnek:

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

Ardından, yönetici sınıfınızda aşağıdaki gibi yalnızca bir işlev uygulamanız gerekir:

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

Görünüm içinde kullanım örneği:

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

Başka bir öneri:

Yöneticiniz yalnızca ayarlayıcıyı ve çevre hesaplamasını (Şekil tarafından da gösterilmektedir) açığa çıkardığından, belirli bir Shape sınıfını başlatırken uygun bir Görünümü başlatabilirsiniz. ÖRNEĞİN:

  • Bir Square ve SquareEditView oluştur;
  • Square örneğini SquareEditView nesnesine iletin;
  • (isteğe bağlı) Bir ShapeManager'a sahip olmak yerine, ana görünümünüzde bir Şekiller listesi tutabilirsiniz;
  • SquareEditView içinde, bir Square'e referans tutarsınız; bu, nesneleri düzenlemek için döküm ihtiyacını ortadan kaldıracaktır.

İlk öneriyi beğendim ve zaten düşündüm, ancak farklı değişkenleri (şamandıra, işaretçiler, diziler) saklamak istiyorsanız oldukça sınırlayıcı. İkinci öneri için, kare zaten somutlaştırılmışsa (görünümde beğendim) bunun bir Kare * nesnesi olduğunu nasıl bilebilirim ? şekilleri saklayan liste bir AbstractShape * döndürür .
ElevenJune

@ElevenJune - evet tüm önerilerin dezavantajları vardır; ilk olarak, daha fazla özellik türü istiyorsanız, basit bir harita yerine daha karmaşık bir şey uygulamanız gerekir. İkinci öneri şekilleri saklama şeklinizi değiştirir; temel şekli listede saklarsınız, ancak aynı zamanda belirli şeklin Görünüm referansını sağlamanız gerekir. Belki senaryonuz hakkında daha fazla ayrıntı verebilirsiniz, bu nedenle bu yaklaşımların bir dinamik_cast gerçekleştirmekten daha iyi olup olmadığını değerlendirebiliriz.
Emerson Cardoso

@ElevenJune - görünüm nesnesine sahip olmanın asıl amacı, GUI'nizin Square tipi bir sınıfla çalıştığını bilmesine gerek olmamasıdır. View nesnesi, nesneyi "görüntülemek" için gerekli olanı sağlar (bunu tanımladığınız ne olursa olsun) ve dahili olarak bunun bir Square sınıfının örneğini kullandığını bilir. GUI yalnızca SquareView örneğiyle etkileşime girer. Böylece, 'Square' sınıfına tıklayamazsınız. Yalnızca bir SquareView sınıfına tıklayabilirsiniz. SquareView'de parametrelerin değiştirilmesi temeldeki Square sınıfını güncelleyecektir ....
Dunk

... Bu yaklaşım ShapeManager sınıfınızdan kurtulmanıza izin verebilir. Bu, tasarımınızı neredeyse kesinlikle basitleştirecektir. Ben her zaman bir sınıf Yöneticisi diyorsan o zaman bunun kötü bir tasarım olduğunu varsayar ve başka bir şey anlıyorum diyorum. Yönetici sınıfları sayısız nedenden ötürü kötüdür, özellikle de tanrı sınıfı sorunu ve kimsenin sınıfın gerçekte ne yaptığını bilmediği, yapamayacağı ve yapamayacağı gerçeği, çünkü Yöneticiler yönettikleri şeyle teğetsel olarak bile her şeyi yapabilirler. Sizi takip eden geliştiricilerin tipik büyük çamur topuna gidenlerden yararlanacağına bahse girebilirsiniz.
Dunk

1
... zaten bu problemle karşılaştı. Neden bir yöneticinin bir şeklin boyutlarını değiştiren bir yönetici olması mantıklı olur? Bir yönetici neden bir şeklin çevresini hesaplar? Eğer çözemediyseniz, "Başka bir öneri" yi seviyorum.
Dunk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.