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
i tools
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 nowej tools
liście. W końcu Tool
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 tylko name
. Później attack()
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:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
Dlaczego to ten sam wzór? Ponieważ bind
nie akceptuje a sockaddr_in*
, akceptuje bardziej ogólne sockaddr*
. Jeśli spojrzysz na definicje tych klas, zobaczymy, że sockaddr
ma tylko jednego członka rodziny, do której przypisaliśmy sin_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ło AF_INET6
, adresem byłby a sockaddr_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 zamiast std::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 ich sin_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że if
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:
- Wolno -
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ę.
- Zbyt wszechstronny - można powiedzieć coś o narzucaniu sobie ograniczeń, gdy robisz coś wyjątkowo niebezpiecznego. Miałem takie systemy, które szukały podłańcucha, który powiedziałby mu, jakiego rodzaju obiekt szuka. Działa to świetnie, dopóki nazwa obiektu przypadkowo nie zawiera tego podłańcucha, i powoduje strasznie tajemniczy błąd. Ponieważ, jak powiedzieliśmy powyżej, potrzebujemy tylko niewielkiej liczby przypadków, nie ma powodu, aby używać masowo przeciążonego narzędzia, takiego jak łańcuchy. To prowadzi do...
- Podatne na błędy - powiedzmy, że będziesz chciał dokonać morderczego szaleństwa, próbując debugować, dlaczego rzeczy nie działają, gdy jeden konsument przypadkowo ustawia nazwę magicznej tkaniny
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:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
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.
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
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 podklasowania Tool
, po prostu umieszczamy na niej wszystkie pola z każdego podtypu.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
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ć:
- Kiedy obiekty są zbyt odmienne - Możesz skończyć z listą prania pól i może nie być jasne, które z nich dotyczą każdego typu obiektu.
- Podczas pracy w sytuacji krytycznej dla pamięci - jeśli potrzebujesz 10 narzędzi, możesz być leniwy z pamięcią. Kiedy musisz zrobić 500 milionów narzędzi, zaczniesz dbać o bity i bajty. Struktury Unii są zawsze większe, niż powinny.
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 jak AF_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ć swoje Sword
, Shield
i MagicCloth
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:
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
Więc jaki jest ten okropny wzór tego boga? Cóż, Tool
ma funkcję wirtualnego accept
. 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. Tak visitor.visit(*this);
działa każdy podtyp. Skomplikowane, ale możemy to pokazać na powyższym przykładzie:
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
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łuje t->accept(v)
funkcję wirtualną. Ponieważ pierwszy obiekt jest tarczą, kończy się wołaniem void Shield::accept(ToolVisitor& visitor)
, które wywołuje visitor.visit(*this);
. Teraz, gdy szukamy, do którego visit
zadzwonić, wiemy już, że mamy tarczę (ponieważ ta funkcja została wywołana), więc skończymy void ToolVisitor::visit(Shield* shield)
na naszym AttackVisitor
. 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żdy ToolVisitor
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żyjesz if
lub switch
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.