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ć Square
s, Circle
s 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 ShapeManager
tworzenie 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 Square
parametry 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_cast
i implementacji pary pobierającej / ustawiającej ShapeManager
dla 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 Job
s dla drukarki 3D (np PrintPatternInZoneJob
, TakePhotoOfZone
itp) z AbstractJob
jak 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 temperaturaTakePhotoOfZone
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
Job
wybrano a, zostanie wyświetlony widget wyświetlający i modyfikujący parametry strefy, ścieżki i wymiarów).
Następnie Job
są umieszczane na liście Job
s, 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
Pattern
lubZone
)
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? JobManager
ma tylko listę AbstractJob*
.
źródło
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
gdziePropertyKey
moż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.Odpowiedzi:
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
AbstractShape
klasa magetType()
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ś,
AbstractShape
ponieważ 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ć
AbstractShape
tylko od niego. Z tego samego powodu nie potrzebuje tejgetType()
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ą zAbstractShape
zajęć, 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 jakoAbstractShape
dodatkowe 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,
ShapeEditView
dla którego maszRectangleEditView
iCircleEditView
implementacje, które przechowują rzeczywiste pola tekstowe dla szerokości / wysokości lub promienia.W pierwszym kroku możesz utworzyć za
RectangleEditView
każdym razem, gdy tworzysz,Rectangle
a następnie wstawić do plikustd::map<AbstractShape*, AbstractShapeView*>
. Jeśli wolisz tworzyć widoki według potrzeb, możesz zamiast tego wykonać następujące czynności: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.
źródło
[rect, rectView]() { rectView.bind(rect); return rectView; }
. Nawiasem mówiąc, należy to oczywiście zrobić w module prezentacji, np. W RectangleCreatedEventHandler.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:
Następnie w klasie menedżera musisz zaimplementować tylko jedną funkcję, jak poniżej:
Przykład użycia w widoku:
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:
źródło