Na przykład mam grę, która ma narzędzia do zwiększenia zdolności gracza:
Tool.h
class Tool{
public:
std::string name;
};
I niektóre narzędzia:
Sword.h
class Sword : public Tool{
public:
Sword(){
this->name="Sword";
}
int attack;
};
Shield.h
class Shield : public Tool{
public:
Shield(){
this->name="Shield";
}
int defense;
};
MagicCloth.h
class MagicCloth : public Tool{
public:
MagicCloth(){
this->name="MagicCloth";
}
int attack;
int defense;
};
I wtedy gracz może mieć kilka narzędzi do ataku:
class Player{
public:
int attack;
int defense;
vector<Tool*> tools;
void attack(){
//original attack and defense
int currentAttack=this->attack;
int currentDefense=this->defense;
//calculate attack and defense affected by tools
for(Tool* tool : tools){
if(tool->name=="Sword"){
Sword* sword=(Sword*)tool;
currentAttack+=sword->attack;
}else if(tool->name=="Shield"){
Shield* shield=(Shield*)tool;
currentDefense+=shield->defense;
}else if(tool->name=="MagicCloth"){
MagicCloth* magicCloth=(MagicCloth*)tool;
currentAttack+=magicCloth->attack;
currentDefense+=magicCloth->shield;
}
}
//some other functions to start attack
}
};
Myślę, że trudno jest zastąpić if-else
wirtualne metody w narzędziach, ponieważ każde narzędzie ma inne właściwości, a każde narzędzie wpływa na atak i obronę gracza, dla których aktualizacja ataku i obrony gracza musi zostać wykonana wewnątrz obiektu gracza.
Ale nie byłem zadowolony z tego projektu, ponieważ zawiera on downcasting, z długim if-else
stwierdzeniem. Czy ten projekt wymaga „korekty”? Jeśli tak, co mogę zrobić, aby to poprawić?
design-patterns
c++
generics
grgrr
źródło
źródło
Odpowiedzi:
Tak, to zapach kodu (w wielu przypadkach).
W twoim przykładzie dość proste jest zastąpienie if / else metodami wirtualnymi:
Teraz nie ma już potrzeby
if
blokowania, dzwoniący może po prostu go używaćOczywiście w bardziej skomplikowanych sytuacjach takie rozwiązanie nie zawsze jest tak oczywiste (ale prawie zawsze możliwe). Ale jeśli dojdziesz do sytuacji, w której nie wiesz, jak rozwiązać sprawę za pomocą metod wirtualnych, możesz ponownie zadać nowe pytanie tutaj w „Programistach” (lub, jeśli staje się ono specyficzne dla języka lub implementacji, w Stackoverflow).
źródło
Sword
w swojej bazie kodu. Możesz po prostunew Tool("sword", swordAttack, swordDefense)
np. Z pliku JSON.Tool
ze wszystkimi możliwymi modyfikatorami, wypełniłem niektórevector<Tool*>
rzeczy odczytane z pliku danych, a następnie po prostu zapętliłem je i zmodyfikowałem statystyki tak jak teraz. Wpadniesz w kłopoty, gdy chcesz, aby jakiś przedmiot dawał np. 10% premii za atak. Być może atool->modify(playerStats)
jest inną opcją.Główny problem z twoim kodem polega na tym, że za każdym razem, gdy wprowadzasz nowy przedmiot, musisz nie tylko pisać i aktualizować kod przedmiotu, ale także modyfikować odtwarzacz (lub gdziekolwiek ten przedmiot jest używany), co sprawia, że całość jest o wiele bardziej skomplikowane.
Zasadniczo myślę, że zawsze jest to trochę podejrzane, kiedy nie możesz polegać na normalnej podklasie / dziedziczeniu i musisz sam wykonać upcasting.
Mógłbym wymyślić dwa możliwe podejścia, które uczynią całość bardziej elastyczną:
Jak wspomniano inni, przenieś członków
attack
idefense
do klasy podstawowej i po prostu zainicjuj ich0
. Może to również podwójnie sprawdzać, czy rzeczywiście jesteś w stanie wymachnąć przedmiotem do ataku lub użyć go do zablokowania ataków.Utwórz system wywołań zwrotnych / zdarzeń. Istnieją różne możliwe podejścia do tego.
Co powiesz na prostotę?
virtual void onEquip(Owner*) {}
ivirtual void onUnequip(Owner*) {}
.virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }
avirtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }
.W porównaniu do samego żądania wartości ataku / obrony w samą porę, nie tylko sprawia to, że całość jest bardziej dynamiczna, ale także oszczędza niepotrzebnych połączeń, a nawet pozwala tworzyć przedmioty, które będą miały trwały wpływ na twoją postać.
Wyobraź sobie na przykład przeklęty pierścień, który po założeniu ukryje statystyki, oznaczając na stałe twoją postać jako przeklętą.
źródło
Chociaż @DocBrown udzieliło dobrej odpowiedzi, nie idzie ono wystarczająco daleko, imho. Zanim zaczniesz oceniać odpowiedzi, powinieneś ocenić swoje potrzeby. Czego naprawdę potrzebujesz ?
Poniżej pokażę dwa możliwe rozwiązania, które oferują różne zalety dla różnych potrzeb.
Pierwszy jest bardzo uproszczony i dostosowany specjalnie do tego, co pokazałeś:
To pozwala bardzo łatwą serializację / deserializację narzędzi (np. Do zapisywania lub tworzenia sieci) i nie wymaga wirtualnej wysyłki. Jeśli twój kod jest wszystkim, co pokazałeś, i nie oczekujesz, że będzie ewoluował znacznie bardziej niż posiadanie większej liczby różnych narzędzi o różnych nazwach i statystykach, tylko w różnych ilościach, to jest to właściwy sposób.
@DocBrown oferuje rozwiązanie, które nadal opiera się na wirtualnej wysyłce, i może być zaletą, jeśli w jakiś sposób specjalizujesz się w narzędziach dla części kodu, które nie zostały pokazane. Jeśli jednak naprawdę chcesz lub chcesz zmienić inne zachowanie, sugerowałbym następujące rozwiązanie:
Kompozycja nad dziedziczeniem
Co jeśli później chcesz mieć narzędzie, które modyfikuje zwinność ? Czy prędkość biegania ? Wydaje mi się, że tworzysz RPG. Jedną z ważnych rzeczy w grach RPG jest możliwość rozszerzenia . Rozwiązania pokazane do tej pory tego nie oferują. Będziesz musiał zmienić
Tool
klasę i dodawać do niej nowe metody wirtualne za każdym razem, gdy potrzebujesz nowego atrybutu.Drugim rozwiązaniem, które pokazuję, jest to, o którym wspomniałem wcześniej w komentarzu - wykorzystuje kompozycję zamiast dziedziczenia i jest zgodne z zasadą „zamknięte dla modyfikacji, otwarte na rozszerzenie *. Jeśli znasz zasady działania systemów encji, niektóre rzeczy będzie wyglądać znajomo (lubię myśleć o kompozycji jako o mniejszym bracie ES).
Zauważ, że to, co pokazuję poniżej, jest znacznie bardziej eleganckie w językach, które zawierają informacje o typie środowiska wykonawczego, takich jak Java lub C #. Dlatego pokazany przeze mnie kod C ++ musi zawierać „księgowość”, która jest po prostu niezbędna, aby kompozycja działała tutaj. Może ktoś z większym doświadczeniem w C ++ jest w stanie zaproponować jeszcze lepsze podejście.
Najpierw ponownie patrzymy na stronę dzwoniącego . W twoim przykładzie jako rozmówcy w
attack
metodzie nie przejmujesz się narzędziami. Ważne są dwie właściwości - punkty ataku i obrony. Tak naprawdę nie obchodzi cię, skąd one pochodzą, i nie obchodzą cię inne właściwości (np. Szybkość biegu, zwinność).Najpierw wprowadzamy nową klasę
Następnie tworzymy nasze pierwsze dwa komponenty
Następnie tworzymy Narzędzie przechowujące zestaw właściwości i udostępniamy właściwości innym osobom.
Zauważ, że w tym przykładzie obsługuję tylko jeden komponent każdego typu - to ułatwia rzeczy. Teoretycznie można również zezwolić na wiele komponentów tego samego typu, ale robi się to brzydko bardzo szybko. Jeden ważny aspekt:
Tool
jest teraz zamknięty dla modyfikacji - nigdy więcej nie dotkniemy źródłaTool
- ale otwarty na rozszerzenie - możemy rozszerzyć zachowanie Narzędzia, modyfikując inne rzeczy i po prostu przekazując do niego inne Komponenty.Teraz potrzebujemy sposobu na wyszukiwanie narzędzi według typów komponentów. Nadal możesz użyć wektora do narzędzi, tak jak w przykładzie kodu:
Możesz również zmienić to na własne
Inventory
klasę i przechowywać tabele wyszukiwania, które znacznie upraszczają pobieranie narzędzi według typu komponentu i unikają powtarzania całej kolekcji.Jakie zalety ma to podejście? W
attack
, ty przetwarzać Narzędzia, które mają dwa składniki - nie dbam o nic innego.Wyobraźmy sobie, że masz
walkTo
metodę, a teraz zdecydujesz, że dobrym pomysłem byłoby, gdyby jakieś narzędzie zyskało możliwość modyfikacji prędkości chodzenia. Nie ma problemu!Najpierw utwórz nowy
Component
:Następnie po prostu dodajesz instancję tego komponentu do narzędzia, które chcesz zwiększyć swoją prędkość budzenia, i zmieniasz
WalkTo
metodę przetwarzania właśnie utworzonego komponentu:Pamiętaj, że dodaliśmy pewne zachowanie do naszych Narzędzi, nie zmieniając w ogóle klasy Tools.
Możesz (i powinieneś) przenieść łańcuchy do makra lub stałej zmiennej statycznej, więc nie musisz wpisywać go wielokrotnie.
Jeśli posuniesz się dalej tym podejściem - np. Stworzysz komponenty, które można dodać do gracza, i stworzysz
Combat
komponent, który oznaczać będzie, że gracz może brać udział w walce, możesz także pozbyć się tejattack
metody i pozwolić sobie na to przez Komponent lub przetwarzane w innym miejscu.Zaletą tego, że gracz będzie mógł również zdobyć Komponenty, byłoby to, że nie trzeba nawet zmieniać gracza, aby nadać mu inne zachowanie. W moim przykładzie możesz stworzyć
Movable
komponent, dzięki czemu nie będziesz musiał zaimplementowaćwalkTo
metody w odtwarzaczu, aby go poruszyć. Po prostu utwórz komponent, podłącz go do odtwarzacza i pozwól, aby ktoś inny go przetworzył.Kompletny przykład można znaleźć w tej liście: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20
To rozwiązanie jest oczywiście nieco bardziej skomplikowane niż inne, które zostały opublikowane. Ale w zależności od tego, jak elastyczny chcesz być, jak daleko chcesz to zrobić, może to być bardzo skuteczne podejście.
Edytować
Niektóre inne odpowiedzi proponują bezpośrednie dziedziczenie (Tworzenie miecza przedłużaniem narzędzia, tworzenie tarczy przedłużaniem narzędzia). Nie sądzę, że jest to scenariusz, w którym dziedziczenie działa bardzo dobrze. Co jeśli zdecydujesz, że blokowanie tarczą w określony sposób może również uszkodzić atakującego? Dzięki mojemu rozwiązaniu możesz po prostu dodać składnik Attack do tarczy i zdać sobie sprawę, że bez żadnych zmian w kodzie. Z dziedziczeniem miałbyś problem. Przedmioty / narzędzia w grach RPG są głównymi kandydatami do składu, a nawet od razu używają systemów jednostek od samego początku.
źródło
Ogólnie rzecz biorąc, jeśli kiedykolwiek będziesz potrzebować
if
(w połączeniu z wymaganiem typu instancji) w dowolnym języku OOP, to znak, że dzieje się coś śmierdzącego. Przynajmniej powinieneś przyjrzeć się bliżej swoim modelom.Zmodelowałbym twoją domenę inaczej.
W twoim przypadku a
Tool
ma aAttackBonus
iDefenseBonus
- które mogą być oba,0
na wypadek, gdyby były bezużyteczne do walki jak pióra lub coś w tym rodzaju.Do ataku masz swoją
baserate
+bonus
z użytej broni. To samo dotyczy obronybaserate
+bonus
.W konsekwencji
Tool
musisz miećvirtual
metodę obliczania boni ataku / obrony.tl; dr
Dzięki lepszemu projektowi można uniknąć włamań
if
.źródło
if
mniej programowania. Głównie w kombinacjach takich jak ifinstanceof
lub coś takiego. Ale jest pozycja, która twierdzi,if
że są kodową komórką i istnieją sposoby na obejście tego. I masz rację, to niezbędny operator, który ma swoje własne prawa.Jak napisano, „pachnie”, ale mogą to być tylko podane przez ciebie przykłady. Przechowywanie danych w ogólnych kontenerach obiektów, a następnie rzutowanie ich w celu uzyskania dostępu do danych nie powoduje automatycznie zapachu kodu. Zobaczysz go używany w wielu sytuacjach. Jednak podczas korzystania z niego powinieneś być świadomy tego, co robisz, jak to robisz i dlaczego. Kiedy patrzę na ten przykład, użycie porównań opartych na łańcuchach, aby powiedzieć mi, jaki obiekt jest tym, co wyzwala mój osobisty miernik zapachu. Sugeruje to, że nie jesteś do końca pewien, co tu robisz (co jest w porządku, ponieważ miałeś mądrość, aby przyjść tutaj do programistów. SE i powiedzieć „hej, nie sądzę, że podoba mi się to, co robię, pomóż odpadam!").
Podstawowym problemem związanym z wzorcem rzutowania danych z takich ogólnych pojemników jest to, że producent danych i konsument danych muszą współpracować, ale może nie być oczywiste, że robią to na pierwszy rzut oka. W każdym przykładzie tego wzoru, śmierdzącego lub nie śmierdzącego, jest to podstawowa kwestia. Jest bardzo możliwe, że następny programista całkowicie nie zdaje sobie sprawy z tego, że robisz ten wzór i łamiesz go przypadkowo, więc jeśli użyjesz tego wzoru, musisz uważać, aby pomóc następnemu deweloperowi. Musisz ułatwić mu niezamierzone złamanie kodu z powodu pewnych szczegółów, o których istnieniu mógł nie wiedzieć.
Na przykład, co jeśli chciałbym skopiować gracza? Jeśli popatrzę tylko na zawartość obiektu odtwarzacza, wygląda to całkiem prosto. Po prostu trzeba skopiować
attack
,defense
itools
zmienne. Proste jak ciasto! Cóż, szybko się przekonam, że użycie wskaźników sprawia, że jest to trochę trudniejsze (w pewnym momencie warto spojrzeć na inteligentne wskaźniki, ale to inny temat). Łatwo to rozwiązać. Po prostu utworzę nowe kopie każdego narzędzia i umieszczę je na mojej nowejtools
liście. W końcuTool
jest to naprawdę prosta klasa z tylko jednym członkiem. Tworzę więc kilka kopii, w tym kopięSword
, ale nie wiedziałem, że to miecz, więc skopiowałem tylkoname
. Późniejattack()
funkcja patrzy na nazwę, widzi, że jest to „miecz”, rzuca go i zdarzają się złe rzeczy!Możemy porównać ten przypadek do innego przypadku w programowaniu gniazd, który używa tego samego wzorca. Mogę skonfigurować funkcję gniazda UNIX w następujący sposób:
Dlaczego to ten sam wzór? Ponieważ
bind
nie akceptuje asockaddr_in*
, akceptuje bardziej ogólnesockaddr*
. Jeśli spojrzysz na definicje tych klas, zobaczymy, żesockaddr
ma tylko jednego członka rodziny, do której przypisaliśmysin_family
*. Rodzina mówi, do którego podtypu powinieneś użyćsockaddr
.AF_INET
informuje, że strukturą adresu jest tak naprawdęsockaddr_in
. Gdyby tak byłoAF_INET6
, adresem byłby asockaddr_in6
, który ma większe pola do obsługi większych adresów IPv6.Jest to identyczne jak w twoim
Tool
przykładzie, z tym wyjątkiem, że używa liczby całkowitej, aby określić, która rodzina zamiaststd::string
. Jednak zamierzam twierdzić, że nie pachnie i staram się to robić z powodów innych niż „to standardowy sposób wykonywania gniazd, więc nie powinien„ wąchać ”. Oczywiście jest to ten sam wzór, który jest dlaczego twierdzę, że przechowywanie danych w obiektach ogólnych i ich rzutowanie nie powoduje automatycznie zapachu kodu, ale istnieją pewne różnice w tym, jak to robią, co czyni je bezpieczniejszym.Podczas korzystania z tego wzorca najważniejszą informacją jest przechwytywanie przekazywania informacji o podklasie od producenta do konsumenta. To właśnie robisz z
name
polem, a gniazda UNIX robią z ichsin_family
polem. To pole jest informacją, której potrzebuje konsument, aby zrozumieć, co faktycznie stworzył producent. We wszystkich przypadkach tego wzorca powinien to być wyliczenie (lub przynajmniej liczba całkowita działająca jak wyliczenie). Czemu? Zastanów się, co zrobi Twój konsument z tymi informacjami. Będą musieli napisać jakieś dużeif
oświadczenie lub…switch
instrukcja, tak jak ty, gdzie określają prawidłowy podtyp, rzutuj go i używaj danych. Z definicji może istnieć tylko niewielka liczba tych typów. Możesz przechowywać go w ciągu, tak jak zrobiłeś, ale ma to wiele wad:std::string
zwykle musi wykonać pamięć dynamiczną, aby zachować ciąg. Musisz także wykonać pełne porównanie tekstu, aby dopasować nazwę za każdym razem, gdy chcesz dowiedzieć się, jaką masz podklasę.MagicC1oth
. Poważnie, takie błędy mogą zająć wiele dni, zanim zdasz sobie sprawę, co się stało.Wyliczenie działa znacznie lepiej. Jest szybki, tani i znacznie mniej podatny na błędy:
Ten przykład pokazuje także
switch
wyrażenie dotyczące wyliczeń, z najważniejszą częścią tego wzorca:default
przypadkiem, który rzuca. Nigdy nie powinieneś wchodzić w taką sytuację, jeśli robisz rzeczy doskonale. Jeśli jednak ktoś doda nowy typ narzędzia i zapomnisz zaktualizować swój kod, aby go obsługiwał, będziesz chciał, aby coś złapało błąd. W rzeczywistości polecam je tak bardzo, że powinieneś je dodać, nawet jeśli ich nie potrzebujesz.Inną ogromną zaletą
enum
jest to, że daje następnemu programistowi pełną listę prawidłowych typów narzędzi, od samego początku. Nie trzeba przedzierać się przez kod, by znaleźć wyspecjalizowaną klasę fletu Boba, której używa w swojej epickiej bitwie z bossem.Tak, wstawiam „pustą” domyślną instrukcję, aby wyraźnie powiedzieć następnemu deweloperowi, czego się spodziewam, jeśli pojawi się jakiś nowy nieoczekiwany typ.
Jeśli to zrobisz, wzór będzie mniej pachniał. Jednak, aby być pozbawionym zapachu, ostatnią rzeczą, którą musisz zrobić, to rozważyć inne opcje. Te obsady są jednymi z bardziej zaawansowanych i niebezpiecznych narzędzi, które masz w repertuarze C ++. Nie powinieneś ich używać, chyba że masz dobry powód.
Jedną z bardzo popularnych alternatyw jest to, co nazywam „strukturą związkową” lub „klasą związkową”. W twoim przypadku byłoby to bardzo dobre dopasowanie. Aby stworzyć jedną z nich, tworzysz
Tool
klasę z wyliczeniem jak poprzednio, ale zamiast podklasowaniaTool
, po prostu umieszczamy na niej wszystkie pola z każdego podtypu.Teraz w ogóle nie potrzebujesz podklas. Wystarczy spojrzeć na
type
pole, aby zobaczyć, które inne pola są faktycznie prawidłowe. Jest to o wiele bezpieczniejsze i łatwiejsze do zrozumienia. Ma to jednak wady. Czasami nie chcesz tego używać:To rozwiązanie nie jest używane przez gniazda UNIX z powodu problemu z podobieństwem, który jest związany z otwartością interfejsu API. Celem gniazd UNIX było stworzenie czegoś, z czym każdy smak UNIXa mógłby współpracować. Każdy smak może zdefiniować listę obsługiwanych rodzin
AF_INET
, a dla każdej z nich będzie krótka lista. Jednak jeśli pojawi się nowy protokół, tak jakAF_INET6
zrobił, może być konieczne dodanie nowych pól. Jeśli zrobiłbyś to z strukturą związkową, ostatecznie stworzyłbyś nową wersję struktury o tej samej nazwie, powodując niekończące się problemy z niekompatybilnością. Właśnie dlatego gniazda UNIX zdecydowały się użyć wzorca rzutowania zamiast struktury związkowej. Jestem pewien, że to rozważali, a fakt, że o tym pomyśleli, jest częścią tego, dlaczego nie pachnie, gdy go używają.Możesz też naprawdę wykorzystać związek. Związki oszczędzają pamięć, ponieważ są tak duże jak największy członek, ale mają własny zestaw problemów. Prawdopodobnie nie jest to opcja dla twojego kodu, ale zawsze jest to opcja, którą powinieneś rozważyć.
Innym interesującym rozwiązaniem jest
boost::variant
. Boost to świetna biblioteka pełna wieloplatformowych rozwiązań wielokrotnego użytku. Jest to prawdopodobnie najlepszy kod C ++, jaki kiedykolwiek napisano. Boost.Variant jest w zasadzie wersją związków w C ++. Jest to pojemnik, który może zawierać wiele różnych typów, ale tylko jeden na raz. Możesz zrobić swojeSword
,Shield
iMagicCloth
klasy, a następnie sprawić, by narzędzie było całkiem sporo!), Ale ten wzór może być niezwykle użyteczny. Wariant jest często używany, na przykład, w parsowaniu drzew, które pobierają ciąg tekstu i dzielą go za pomocą gramatyki reguł.boost::variant<Sword, Shield, MagicCloth>
, co oznacza, że zawiera jedną z tych trzech typów. Nadal występuje ten sam problem z przyszłą kompatybilnością, która uniemożliwia korzystanie z gniazd UNIX (nie wspominając o gniazdach UNIX C, wcześniejszychboost
Ostatecznym rozwiązaniem, na które zaleciłbym przyjrzenie się przed zanurzeniem i zastosowaniem ogólnego sposobu rzucania obiektów, jest wzorzec projektowy Visitor . Visitor to potężny wzorzec projektowy, który wykorzystuje spostrzeżenie, że wywołanie funkcji wirtualnej skutecznie wykonuje rzutowanie, którego potrzebujesz, i robi to za Ciebie. Ponieważ kompilator to robi, nigdy nie może być źle. Dlatego zamiast przechowywać wyliczenie, Visitor używa abstrakcyjnej klasy bazowej, która ma vtable, która wie, jakiego typu jest obiekt. Następnie tworzymy zgrabne połączenie z podwójną pośrednią, które działa:
Więc jaki jest ten okropny wzór tego boga? Cóż,
Tool
ma funkcję wirtualnegoaccept
. Jeśli podasz go odwiedzającemu, oczekuje się, że się odwróci i wywoła odpowiedniąvisit
funkcję dla tego gościa dla danego typu. Takvisitor.visit(*this);
działa każdy podtyp. Skomplikowane, ale możemy to pokazać na powyższym przykładzie:Co się tu dzieje? Tworzymy gościa, który wykona dla nas trochę pracy, gdy tylko dowie się, jaki typ obiektu odwiedza. Następnie iterujemy listę narzędzi. Na przykład, powiedzmy, że pierwszym obiektem jest
Shield
, ale nasz kod jeszcze tego nie wie. Wywołujet->accept(v)
funkcję wirtualną. Ponieważ pierwszy obiekt jest tarczą, kończy się wołaniemvoid Shield::accept(ToolVisitor& visitor)
, które wywołujevisitor.visit(*this);
. Teraz, gdy szukamy, do któregovisit
zadzwonić, wiemy już, że mamy tarczę (ponieważ ta funkcja została wywołana), więc skończymyvoid ToolVisitor::visit(Shield* shield)
na naszymAttackVisitor
. To teraz uruchamia poprawny kod, aby zaktualizować naszą obronę.Gość jest nieporęczny. Jest tak niezgrabny, że prawie wydaje mi się, że ma swój własny zapach. Bardzo łatwo jest pisać złe wzorce odwiedzających. Ma jednak jedną ogromną przewagę , jakiej nie ma żaden inny. Jeśli dodamy nowy typ narzędzia, musimy dodać
ToolVisitor::visit
dla niego nową funkcję. Gdy tylko to zrobimy, każdyToolVisitor
program odmówi kompilacji, ponieważ brakuje mu funkcji wirtualnej. Dzięki temu bardzo łatwo jest złapać wszystkie przypadki, w których coś przeoczyliśmy. Znacznie trudniej jest zagwarantować, że jeśli użyjeszif
lubswitch
oświadczeń do wykonania pracy. Te zalety są na tyle dobre, że Visitor znalazł małą niszę w generatorach scen graficznych 3D. Potrzebują dokładnie takiego zachowania, jakie oferuje Odwiedzający, więc działa świetnie!W sumie pamiętaj, że te wzorce utrudniają następny programista. Poświęć czas, aby im to ułatwić, a kod nie będzie pachniał!
* Technicznie rzecz biorąc, jeśli spojrzysz na specyfikację, sockaddr ma jednego członka o nazwie
sa_family
. Na poziomie C robi się coś trudnego, co nie ma dla nas znaczenia. Zapraszamy do zapoznania się z faktyczną implementacją, ale dla tej odpowiedzi zamierzam użyćsa_family
sin_family
i innych całkowicie zamiennie, używając tego, który jest najbardziej intuicyjny dla prozy, ufając, że oszustwo C zajmuje się nieistotnymi szczegółami.źródło
Ogólnie rzecz biorąc, unikam implementowania kilku klas / dziedziczenia, jeśli chodzi tylko o komunikację danych. Możesz trzymać się jednej klasy i stamtąd wdrażać wszystko. Dla twojego przykładu to wystarczy
Prawdopodobnie spodziewasz się, że twoja gra będzie implementować kilka rodzajów mieczy itp., Ale będziesz miał inne sposoby na wdrożenie tego. Eksplozja klasowa rzadko jest najlepszą architekturą. Nie komplikuj.
źródło
Jak już wspomniano, jest to poważny zapach kodu. Można jednak rozważyć źródło problemu polegającego na zastosowaniu dziedziczenia zamiast kompozycji w projekcie.
Na przykład, biorąc pod uwagę to, co nam pokazałeś, wyraźnie masz 3 koncepcje:
Zauważ, że twoja czwarta klasa jest tylko kombinacją dwóch ostatnich koncepcji. Sugerowałbym więc użycie do tego kompozycji.
Potrzebujesz struktury danych do reprezentowania informacji potrzebnych do ataku. Potrzebujesz struktury danych reprezentującej informacje potrzebne do obrony. Na koniec potrzebujesz struktury danych do reprezentowania rzeczy, które mogą mieć lub nie mieć jedną lub obie z tych właściwości:
źródło
Attack
iDefense
staje się bardziej skomplikowana bez zmiany interfejsuTool
.Tool
całkowicie zamknąć dla modyfikacji, jednocześnie pozostawiając ją otwartą do rozszerzenia.Tool
bez modyfikacji . A jeśli mam prawo to zmodyfikować, nie widzę potrzeby stosowania dowolnych komponentów.Dlaczego nie tworzenie abstrakcyjnych metod
modifyAttack
imodifyDefense
wTool
klasie? Następnie każde dziecko będzie miało własną implementację, a ty nazywasz to eleganckim sposobem:Przekazanie wartości jako odniesienia pozwoli zaoszczędzić zasoby, jeśli możesz:
źródło
Jeśli używa się polimorfizmu, zawsze najlepiej jest, jeśli cały kod, który dba o to, która klasa jest używana, znajduje się w samej klasie. Oto jak bym to kodował:
Ma to następujące zalety:
źródło
Myślę, że jednym ze sposobów na rozpoznanie wad tego podejścia jest rozwinięcie swojego pomysłu do jego logicznej konkluzji.
To wygląda jak gra, więc na pewnym etapie prawdopodobnie zaczniesz martwić się wydajnością i zamienisz porównania ciągów znaków na
int
lubenum
. Gdy lista przedmiotów się wydłuża,if-else
zaczyna się robić nieporęczna, więc możesz rozważyć przekształcenie jej w postaćswitch-case
. W tym momencie masz też dość ścianę tekstu, więc możesz wyciszyć akcję w każdej z nich,case
tworząc osobną funkcję.Po osiągnięciu tego punktu struktura kodu zaczyna wyglądać znajomo - zaczyna wyglądać jak homebrew, ręcznie toczony vtable * - podstawowa struktura, na której zwykle wdrażane są metody wirtualne. Poza tym jest to vtable, który należy ręcznie aktualizować i utrzymywać za każdym razem, gdy dodajesz lub modyfikujesz typ elementu.
Trzymając się „rzeczywistych” funkcji wirtualnych, możesz zachować implementację zachowania każdego elementu w obrębie samego elementu. Możesz dodawać dodatkowe elementy w bardziej samodzielny i spójny sposób. A gdy to wszystko robisz, to kompilator będzie zajmował się implementacją dynamicznej wysyłki, a nie Ty.
Aby rozwiązać konkretny problem: próbujesz napisać prostą parę wirtualnych funkcji do aktualizacji ataku i obrony, ponieważ niektóre elementy wpływają tylko na atak, a niektóre tylko na obronę. Sztuczka w prostym przypadku takim jak ten, aby zaimplementować oba te zachowania, ale w niektórych przypadkach bez efektu.
GetDefenseBonus()
może wrócić0
lubApplyDefenseBonus(int& defence)
pozostawićdefence
bez zmian. To, jak sobie poradzisz, będzie zależeć od tego, jak chcesz poradzić sobie z innymi działaniami, które mają wpływ. W bardziej skomplikowanych przypadkach, w których zachowanie jest bardziej zróżnicowane, możesz po prostu połączyć działanie w jedną metodę.* (Aczkolwiek transponowane względem typowej implementacji)
źródło
Posiadanie bloku kodu, który zna wszystkie możliwe „narzędzia”, nie jest świetnym projektem (zwłaszcza, że skończysz z wieloma takimi blokami w kodzie); ale żadna z nich nie ma podstawowego
Tool
z kodami pośredniczącymi dla wszystkich możliwych właściwości narzędzia: terazTool
klasa musi wiedzieć o wszystkich możliwych zastosowaniach.Co każde narzędzie wie co to może przyczynić się do znaku, który go używa. Tak stanowią jedną metodę dla wszystkich narzędzi
giveto(*Character owner)
. Dostosuje odpowiednio statystyki gracza, nie wiedząc, co mogą zrobić inne narzędzia, a co najlepsze, nie musi też wiedzieć o nieistotnych właściwościach postaci. Na przykład, tarcza nawet nie musi wiedzieć o atrybutachattack
,invisibility
,health
itd. Wszystko, co potrzebne, aby zastosować to narzędzie jest postać wspierać atrybuty, że obiekt wymaga. Jeśli spróbujesz dać osiołowi miecz, a osioł nie maattack
statystyk, pojawi się błąd.Narzędzia powinny również mieć
remove()
metodę, która odwraca ich wpływ na właściciela. Jest to trochę trudne (możliwe jest, aby skończyć z narzędziami, które pozostawiają niezerowy efekt, gdy zostaną podane, a następnie zabrane), ale przynajmniej są zlokalizowane dla każdego narzędzia.źródło
Nie ma odpowiedzi, która mówi, że to nie pachnie, więc ja będę tą opinią; ten kod jest w porządku! Moja opinia opiera się na fakcie, że czasem łatwiej jest przejść dalej i pozwolić na stopniowe zwiększanie umiejętności w miarę tworzenia nowych rzeczy. Możesz utknąć na kilka dni w tworzeniu idealnej architektury, ale prawdopodobnie nikt nawet nie zobaczy go w akcji, ponieważ nigdy nie ukończyłeś projektu. Twoje zdrowie!
źródło