Prawidłowy projekt, aby uniknąć użycia dynamic_cast?

9

Po przeprowadzeniu niektórych badań nie mogę znaleźć prostego przykładu rozwiązującego często spotykany problem.

Powiedzmy, że chcę utworzyć małą aplikację, w której mogę tworzyć Squares, Circles i inne kształty, wyświetlać je na ekranie, modyfikować ich właściwości po ich wybraniu, a następnie obliczać wszystkie ich obwody.

Zrobiłbym klasę modelu w ten sposób:

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

(Wyobraź sobie, że mam więcej klas kształtów: trójkąty, sześciokąty, za każdym razem ich zmienne miotające oraz powiązane z nimi funkcje pobierające i ustawiające. Problemy, z którymi się spotkałem, miały 8 podklas, ale dla przykładu zatrzymałem się na 2)

Mam teraz ShapeManagertworzenie i tworzenie wszystkich kształtów w tablicy:

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

Wreszcie mam widok ze spinboxami, aby zmienić każdy parametr dla każdego typu kształtu. Na przykład, kiedy wybieram kwadrat na ekranie, widget parametrów wyświetla tylko Squareparametry powiązane (dzięki AbstractShape::getType()) i proponuje zmianę szerokości kwadratu. Aby to zrobić, potrzebuję funkcji pozwalającej mi modyfikować szerokość w ShapeManager, i tak to robię:

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

Czy istnieje lepszy projekt pozwalający uniknąć użycia dynamic_casti implementacji pary pobierającej / ustawiającej ShapeManagerdla każdej zmiennej podklasy, którą mogę mieć? Próbowałem już użyć szablonu, ale nie udało mi się .


Problem mam w obliczu tak naprawdę nie jest z kształtami, ale z różnych Jobs dla drukarki 3D (np PrintPatternInZoneJob, TakePhotoOfZoneitp) z AbstractJobjak ich klasy bazowej. Metoda wirtualna jest execute()i nie jest getPerimeter(). Jedyne, czego potrzebuję, aby użyć konkretnego użycia, to wypełnienie konkretnych informacji, których potrzebuje praca :

  • PrintPatternInZone potrzebuje listy punktów do wydrukowania, pozycji strefy, niektórych parametrów drukowania, takich jak temperatura

  • TakePhotoOfZone potrzebuje strefy, którą należy zrobić, ścieżki, w której zdjęcie zostanie zapisane, wymiarów itp.

Kiedy zadzwonię execute(), Jobs wykorzysta konkretne informacje, które musi wykonać, aby wykonać akcję, którą powinni wykonać.

Jedynym razem, gdy muszę użyć konkretnego typu zadania, jest wypełnienie lub wyświetlenie tych informacji (jeśli TakePhotoOfZone Jobwybrano a, zostanie wyświetlony widget wyświetlający i modyfikujący parametry strefy, ścieżki i wymiarów).

Następnie Jobsą umieszczane na liście Jobs, które podejmują pierwszą pracę, wykonują ją (dzwoniąc AbstractJob::execute()), przechodzą do następnej, z przerwami i do końca listy. (Dlatego używam dziedziczenia).

Do przechowywania różnych typów parametrów używam JsonObject:

  • zalety: taka sama struktura dla każdego zadania, brak dynamicznego nadawania podczas ustawiania lub odczytu parametrów

  • problem: nie można przechowywać wskaźników (do Patternlub Zone)

Czy uważasz, że istnieje lepszy sposób przechowywania danych?

Potem , jak można przechowywać typ betonieJob go używać kiedy muszę zmodyfikować parametry specyficzne tego typu? JobManagerma tylko listę AbstractJob*.

ElevenJune
źródło
5
Wygląda na to, że ShapeManager stanie się klasą Boga, ponieważ będzie zawierał wszystkie metody ustawiające dla wszystkich typów kształtów.
Emerson Cardoso
Czy rozważałeś projekt „torby na zakupy”? Na przykład, changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)gdzie PropertyKeymoże być wyliczenie lub ciąg znaków, a „Szerokość” (co oznacza, że ​​wywołanie funkcji ustawiającej zaktualizuje wartość szerokości) należy do jednej z dozwolonych wartości.
rwong
Chociaż niektórzy uważają, że torba na nieruchomości jest anty-wzorem OO, zdarzają się sytuacje, gdy używanie torby na rzeczy upraszcza projektowanie, a każda inna alternatywa komplikuje sprawę. Chociaż, aby ustalić, czy torba właściwości jest odpowiednia dla twojego przypadku użycia, potrzeba więcej informacji (takich jak sposób, w jaki kod GUI współdziała z getter / setter).
rwong
Rozważyłem projekt torby właściwości (chociaż nie znałem jego nazwy), ale z pojemnikiem obiektu JSON. Z pewnością może działać, ale myślałem, że to nie jest elegancki design i że może istnieć lepsza opcja. Dlaczego jest uważany za anty-wzór OO?
ElevenJune
Na przykład, jeśli chcę zapisać wskaźnik, aby użyć go później, jak to zrobić?
ElevenJune

Odpowiedzi:

10

Chciałbym rozwinąć „inną sugestię” Emersona Cardoso, ponieważ uważam, że jest to właściwe podejście w ogólnym przypadku - chociaż oczywiście możesz znaleźć inne rozwiązania, które lepiej pasują do konkretnego problemu.

Problem

W twoim przykładzie AbstractShapeklasa ma getType()metodę, która w zasadzie identyfikuje konkretny typ. Zazwyczaj jest to znak, że nie masz dobrej abstrakcji. W końcu celem abstrakcji nie jest dbanie o szczegóły konkretnego typu.

Ponadto, jeśli nie jesteś zaznajomiony z tym, powinieneś przeczytać na temat zasady otwartej / zamkniętej. Często wyjaśnia się to przykładem kształtu, dzięki czemu poczujesz się jak w domu.

Przydatne abstrakcje

Zakładam, że wprowadziłeś, AbstractShapeponieważ uważasz, że jest to przydatne do czegoś. Najprawdopodobniej część aplikacji musi znać obwód kształtów, niezależnie od tego, jaki jest kształt.

To miejsce, w którym abstrakcja ma sens. Ponieważ ten moduł nie dotyczy konkretnych kształtów, może zależeć AbstractShapetylko od niego. Z tego samego powodu nie potrzebuje tej getType()metody - więc powinieneś się jej pozbyć.

Inne części aplikacji będą działać tylko z określonym rodzajem kształtu, np Rectangle. Te obszary nie skorzystają z AbstractShapezajęć, więc nie powinieneś z nich korzystać. Aby przekazać tylko odpowiedni kształt do tych części, musisz osobno przechowywać kształty betonowe. (Możesz przechowywać je jako AbstractShapedodatkowe lub łączyć je w locie).

Minimalizacja zużycia betonu

Nie można tego obejść: w niektórych miejscach potrzebujesz rodzajów betonu - przynajmniej podczas budowy. Jednak czasami najlepiej jest ograniczyć użycie konkretnych rodzajów do kilku dobrze określonych obszarów. Te oddzielne obszary mają na celu wyłącznie radzenie sobie z różnymi typami - podczas gdy cała logika aplikacji jest poza nimi.

Jak to osiągasz? Zwykle przez wprowadzenie większej liczby abstrakcji - które mogą, ale nie muszą odzwierciedlać istniejące abstrakcje. Na przykład GUI nie musi tak naprawdę wiedzieć, z jakim kształtem ma do czynienia. Musi tylko wiedzieć, że na ekranie jest obszar, w którym użytkownik może edytować kształt.

Definiujesz więc streszczenie, ShapeEditViewdla którego masz RectangleEditViewi CircleEditViewimplementacje, które przechowują rzeczywiste pola tekstowe dla szerokości / wysokości lub promienia.

W pierwszym kroku możesz utworzyć za RectangleEditViewkażdym razem, gdy tworzysz, Rectanglea następnie wstawić do pliku std::map<AbstractShape*, AbstractShapeView*>. Jeśli wolisz tworzyć widoki według potrzeb, możesz zamiast tego wykonać następujące czynności:

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

Tak czy inaczej, kod poza logiką tworzenia nie będzie musiał zajmować się konkretnymi kształtami. Oczywiście w ramach zniszczenia kształtu musisz usunąć fabrykę. Oczywiście ten przykład jest zbyt uproszczony, ale mam nadzieję, że pomysł jest jasny.

Wybór odpowiedniej opcji

W bardzo prostych aplikacjach może się okazać, że brudne (odlewane) rozwiązanie zapewnia najwyższy zysk.

Jawne utrzymywanie osobnych list dla każdego rodzaju betonu jest prawdopodobnie dobrym rozwiązaniem, jeśli aplikacja zajmuje się głównie kształtami betonu, ale ma pewne uniwersalne części. Tutaj sens ma abstrakty tylko wtedy, gdy wymaga tego wspólna funkcjonalność.

Wszystko idzie dobrze, jeśli masz dużo logiki, która działa na kształtach, a dokładny rodzaj kształtu jest naprawdę szczegółem twojej aplikacji.

doubleYou
źródło
Naprawdę podoba mi się twoja odpowiedź, doskonale opisałeś problem. Problem, przed którym stoję, nie dotyczy w rzeczywistości kształtów, ale różnych zadań dla drukarki 3D (np. PrintPatternInZoneJob, TakePhotoOfZone itp.) Z klasą AbstractJob. Metodą wirtualną jest execute (), a nie getPerimeter (). Jedyne, czego potrzebuję, aby użyć konkretnego użycia, to wypełnienie określonych informacji potrzebnych w pracy (lista punktów, pozycja, temperatura itp.) Określonym widżetem. Dołączanie widoku do każdej pracy nie wydaje się być w tym konkretnym przypadku, ale nie widzę, jak dostosować swoją wizję do mojego PB.
ElevenJune
Jeśli nie chcesz zachować oddzielne listy, można użyć viewSelector zamiast viewFactory: [rect, rectView]() { rectView.bind(rect); return rectView; }. Nawiasem mówiąc, należy to oczywiście zrobić w module prezentacji, np. W RectangleCreatedEventHandler.
podwójnyYou 10'18
3
To powiedziawszy, staraj się nie nadużytkować tego. Korzyści z abstrakcji wciąż muszą przewyższać koszty dodatkowego upierzenia. Czasami lepsza może być dobrze rzucona obsada lub osobna logika.
podwójnyYou 10'18
2

Jednym z podejść byłoby uogólnienie , aby uniknąć rzutowania na określone typy .

Możesz zaimplementować podstawowy getter / setter właściwości float „ wymiar ” w klasie bazowej, który ustawia wartość na mapie w oparciu o określony klucz dla nazwy właściwości. Przykład poniżej:

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

Następnie w klasie menedżera musisz zaimplementować tylko jedną funkcję, jak poniżej:

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

Przykład użycia w widoku:

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

Kolejna sugestia:

Ponieważ Twój menedżer udostępnia tylko narzędzie do ustawiania i obliczania obwodu (które są również widoczne w programie Shape), możesz po prostu utworzyć prawidłowy widok podczas tworzenia określonej klasy Shape. NA PRZYKŁAD:

  • Utwórz instancję Square i SquareEditView;
  • Przekaż instancję Square do obiektu SquareEditView;
  • (opcjonalnie) Zamiast mieć ShapeManager, w głównym widoku nadal możesz przechowywać listę Kształty;
  • W SquareEditView zachowujesz odniesienie do kwadratu; wyeliminowałoby to konieczność rzutowania do edycji obiektów.
Emerson Cardoso
źródło
Podoba mi się pierwsza sugestia i już o niej pomyślałem, ale jest dość ograniczająca, jeśli chcesz przechowywać różne zmienne (zmiennoprzecinkowe, wskaźniki, tablice). W przypadku drugiej sugestii, jeśli kwadrat jest już utworzony (utworzyłem go na widoku), skąd mam wiedzieć, że jest to obiekt Square * ? lista przechowująca kształty zwraca AbstractShape * .
ElevenJune
@ElevenJune - tak, wszystkie sugestie mają swoje wady; po pierwsze, jeśli potrzebujesz więcej rodzajów właściwości, musisz zaimplementować coś bardziej złożonego niż prostą mapę. Druga sugestia zmienia sposób przechowywania kształtów; zapisujesz kształt podstawowy na liście, ale jednocześnie musisz podać odniesienie określonego kształtu do Widoku. Być może możesz podać więcej szczegółów na temat swojego scenariusza, abyśmy mogli ocenić, czy te podejścia są lepsze niż po prostu wykonanie dynamicznej transmisji.
Emerson Cardoso
@ElevenJune - cały sens posiadania obiektu widoku polega na tym, że GUI nie musi wiedzieć, że działa z klasą typu Square. Obiekt widoku zapewnia to, co jest konieczne do „wyświetlenia” obiektu (cokolwiek go zdefiniujesz) i wewnętrznie wie, że używa instancji klasy Square. GUI współdziała tylko z instancją SquareView. Dlatego nie możesz kliknąć klasy „Kwadrat”. Możesz kliknąć tylko klasę SquareView. Zmiana parametrów SquareView spowoduje aktualizację podstawowej klasy Square ....
Dunk
... Takie podejście może bardzo dobrze pozbyć się klasy ShapeManager. To prawie na pewno uprości twój projekt. Zawsze mówię, że jeśli nazwiesz klasę Menedżerem, to załóż, że to zły projekt i wymyśl coś innego. Klasy menedżerskie są złe z wielu powodów, w szczególności problemu klasy boga i faktu, że nikt nie wie, co klasa faktycznie robi, może i nie może zrobić, ponieważ menedżerowie mogą robić wszystko, nawet stycznie związane z tym, czym zarządzają. Możesz się założyć, że ci programiści, którzy podążą za tobą, skorzystają z tego, co prowadzi do typowej wielkiej kuli błota.
Dunk
1
... już napotkałeś ten problem. Dlaczego, u licha, miałoby sens mieć menedżera, który zmienia wymiary kształtu? Dlaczego menedżer miałby obliczyć obwód kształtu? Na wypadek, gdybyś tego nie rozgryzł, podoba mi się „Kolejna sugestia”.
Dunk