Rozważ poniższy przykład. Każda zmiana wyliczenia ColorChoice wpływa na wszystkie podklasy IWindowColor.
Czy wytryski powodują kruche interfejsy? Czy istnieje coś lepszego niż wyliczenie, które pozwala na większą elastyczność polimorficzną?
enum class ColorChoice
{
Blue = 0,
Red = 1
};
class IWindowColor
{
public:
ColorChoice getColor() const=0;
void setColor( const ColorChoice value )=0;
};
Edycja: przepraszam za użycie koloru jako mojego przykładu, nie o to chodzi w tym pytaniu. Oto inny przykład, który pozwala uniknąć czerwonego śledzia i zawiera więcej informacji na temat tego, co rozumiem przez elastyczność.
enum class CharacterType
{
orc = 0,
elf = 1
};
class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
CharacterType getCharacterType() const;
void setCharacterType( const CharacterType value );
};
Ponadto, wyobraź sobie, że uchwyty do odpowiedniej podklasy ISomethingThatNeedsToKnowWhatTypeOfCharacter są wydawane na podstawie fabrycznego wzorca projektowego. Teraz mam interfejs API, którego nie można rozszerzyć w przyszłości dla innej aplikacji, w której dopuszczalnymi typami znaków są {człowiek, karzeł}.
Edycja: Aby być bardziej konkretnym na temat tego, nad czym pracuję. Projektuję silne wiązanie tej specyfikacji ( MusicXML ) i używam klas enum do reprezentowania tych typów w specyfikacji, które są zadeklarowane za pomocą xs: enumeration. Próbuję myśleć o tym, co się stanie, gdy pojawi się kolejna wersja (4.0). Czy moja biblioteka klas może pracować w trybie 3.0 i 4.0? Jeśli następna wersja jest w 100% kompatybilna wstecz, to może. Ale jeśli wartości wyliczenia zostały usunięte ze specyfikacji, to jestem martwy w wodzie.
źródło
Odpowiedzi:
Przy właściwym stosowaniu wyliczenia są znacznie bardziej czytelne i niezawodne niż „magiczne liczby”, które zastępują. Zwykle nie widzę, aby kod był bardziej kruchy. Na przykład:
value
jest prawidłową wartością koloru, czy nie. Kompilator już to zrobił.enum class
funkcja we współczesnym C ++ pozwala nawet zmusić ludzi do pisania pierwszego zamiast drugiego.Jednak użycie wyliczenia koloru jest wątpliwe, ponieważ w wielu (większości?) Sytuacjach nie ma powodu, aby ograniczać użytkownika do tak małego zestawu kolorów; równie dobrze możesz pozwolić im przekazywać dowolne wartości RGB. W projektach, nad którymi pracuję, mała lista kolorów, takich jak ta, pojawiałaby się tylko jako część zestawu „motywów” lub „stylów”, które powinny działać jak cienka abstrakcja nad konkretnymi kolorami.
Nie jestem pewien, do czego zmierza twoje pytanie dotyczące „elastyczności polimorficznej”. Wyliczenia nie mają żadnego kodu wykonywalnego, więc nie ma nic, co można by uczynić polimorficznym. Być może szukasz wzorca poleceń ?
Edycja: Po edycji nadal nie jestem pewien, jakiego rodzaju rozszerzalności szukasz, ale nadal uważam, że wzorzec poleceń jest najbliższą rzeczą, jaką uzyskasz w przypadku „wyliczenia polimorficznego”.
źródło
Nie, nie ma. Istnieją dwa przypadki: realizatorzy też
zapisz, zwróć i przekaż wartości wyliczenia, nigdy na nich nie operując, w takim przypadku zmiany wyliczenia nie mają na nie wpływu, lub
działają na poszczególnych wartościach wyliczeniowych, w którym to przypadku wszelkie zmiany w wyliczeniu muszą oczywiście, oczywiście, nieuchronnie, koniecznie , zostać uwzględnione z odpowiednią zmianą logiki implementatora.
Jeśli umieścisz „Kula”, „Prostokąt” i „Piramida” w wyliczeniu „Kształt” i przekażesz taki enum do
drawSolid()
funkcji, którą napisałeś, aby narysować odpowiednią bryłę, a następnie pewnego ranka zdecydujesz się dodać „ Ikositetrahedron ”do wyliczenia, nie można oczekiwać, żedrawSolid()
funkcja pozostanie niezmieniona; Jeśli spodziewałeś się, że w jakiś sposób narysuje dwunastościany bez konieczności pisania właściwego kodu do rysowania dwunastościanów, to twoja wina, a nie wina wyliczenia. Więc:Nie, nie robią tego. To, co powoduje kruche interfejsy, to programiści myślący o sobie jako ninja, próbujący skompilować swój kod bez włączonej wystarczającej liczby ostrzeżeń. Następnie kompilator nie ostrzega ich, że w ich
drawSolid()
funkcji znajduje sięswitch
instrukcja, w której brakujecase
klauzuli dla nowo dodanej wartości wyliczenia „Ikositetrahedron”.Sposób, w jaki powinien działać, jest analogiczny do dodawania nowej czystej metody wirtualnej do klasy bazowej: musisz następnie zaimplementować tę metodę na każdym spadkobiercy, w przeciwnym razie projekt nie buduje i nie powinien.
Prawdę mówiąc, wyliczenia nie są konstrukcją obiektową. Są bardziej pragmatycznym kompromisem między paradygmatem obiektowym a paradygmatem programowania strukturalnego.
Czysto zorientowany obiektowo sposób robienia rzeczy wcale nie ma wyliczeń, a zamiast tego ma przedmioty do końca.
Zatem czysto obiektowym sposobem implementacji przykładu z ciałami stałymi jest oczywiście użycie polimorfizmu: zamiast mieć jedną monstrualną scentralizowaną metodę, która wie, jak narysować wszystko i trzeba powiedzieć, którą bryłę narysować, deklarujesz „ Solidna klasa z abstrakcyjną (czystą wirtualną)
draw()
metodą, a następnie dodajesz podklasy „Kula”, „Prostokąt” i „Piramida”, z których każda ma własną implementację, zdraw()
której umie rysować.W ten sposób, kiedy wprowadzisz podklasę „Ikositetrahedron”, będziesz musiał po prostu podać jej
draw()
funkcję, a kompilator przypomni ci o tym, nie pozwalając na utworzenie instancji „Icositetrahedron” w innym przypadku.źródło
default:
klauzuli powinno to zrobić. Szybkie wyszukiwanie na ten temat nie przyniosło żadnych konkretnych rezultatów, więc może to być przydatne jako przedmiot innegoprogrammers SE
pytania.Pyramid
faktycznie wiedziałby, jak zrobićdraw()
piramidę. W najlepszym wypadku może pochodzićSolid
i miećGetTriangles()
metodę, którą można przekazać doSolidDrawer
usługi. Myślałem, że uciekamy od przykładów obiektów fizycznych jako przykładów obiektów w OOP.Wyliczenia nie tworzą kruchych interfejsów. Niewłaściwe użycie enum.
Do czego służą wyliczenia?
Wyliczenia są zaprojektowane do użycia jako zestawy znaczących nazw stałych. Należy ich używać, gdy:
Dobre wykorzystanie wyliczeń:
System.DayOfWeek
) O ile nie masz do czynienia z jakimś niezwykle niejasnym kalendarzem, będą tylko 7 dni w tygodniu.System.ConsoleColor
) Niektórzy mogą się z tym nie zgadzać, ale .Net zdecydował się to zrobić z jakiegoś powodu. W systemie konsoli .Net dostępnych jest tylko 16 kolorów do użycia przez konsolę. Te 16 kolorów odpowiada starszej palecie kolorów znanej jako CGA lub „Color Graphics Adapter” . Żadne nowe wartości nigdy nie zostaną dodane, co oznacza, że w rzeczywistości jest to rozsądne zastosowanie wyliczenia.Thread.State
) Projektanci Javy zdecydowali, że w modelu wątków Java zawsze będzie ustalony zestaw stanów, w którychThread
może znajdować się a, aby uprościć sprawy, te różne stany są reprezentowane jako wyliczenia . Oznacza to, że wiele sprawdzeń opartych na stanie jest prostymi „if” i przełącznikami, które w praktyce działają na wartościach całkowitych, bez programisty, który musi się martwić o rzeczywiste wartości.System.Text.RegularExpressions.RegexOptions
) Flagi bitowe są bardzo powszechnym zastosowaniem wyliczeń. Tak powszechne, że w .Net wszystkie wyliczenia mająHasFlag(Enum flag)
wbudowaną metodę. Obsługują one również operatory bitowe i istniejeFlagsAttribute
możliwość oznaczenia wyliczenia jako przeznaczonego do użycia jako zestawu bitflagów. Używając wyliczenia jako zestawu flag, możesz reprezentować grupę wartości boolowskich w pojedynczej wartości, a także mieć wyraźne nazwy flag dla wygody. Byłoby to niezwykle korzystne do reprezentowania flag rejestru statusu w emulatorze lub do reprezentowania uprawnień do pliku (odczyt, zapis, wykonywanie) lub w prawie każdej sytuacji, w której zestaw powiązanych opcji nie wyklucza się wzajemnie.Niewłaściwe użycie wyliczeń:
True
flagiFalse
). To zachowanie może być dalej wspierane przez użycie specjalistycznych funkcji (tj.IsTrue(flags)
IIsFalse(flags)
).źródło
RegexOptions
msdn.microsoft.com/en-us/library/...Wyliczenia to doskonała poprawa w porównaniu z magicznymi numerami identyfikacyjnymi dla zamkniętych zestawów wartości, które nie są powiązane z wieloma funkcjami. Zwykle nie obchodzi cię, jaki numer jest faktycznie związany z wyliczeniem; w tym przypadku łatwo jest rozszerzyć, dodając nowe wpisy na końcu, nie powinna wynikać kruchość.
Problem występuje, gdy masz znaczną funkcjonalność związaną z wyliczeniem. Oznacza to, że masz taki kod:
Jeden lub dwa z nich są w porządku, ale gdy tylko pojawi się tego rodzaju znaczący wyliczenie,
switch
stwierdzenia te mają tendencję do mnożenia się jak tribble. Teraz za każdym razem, gdy przedłużasz wyliczanie, musisz wytropić wszystkie powiązaneswitch
es, aby upewnić się, że wszystko pokryłeś. To jest kruche. Źródła takie jak Clean Code sugerują, że powinieneś mieć jedenswitch
najwyżej enum.Zamiast tego w tym przypadku powinieneś użyć zasad OO i stworzyć interfejs dla tego typu. Nadal możesz zachować wyliczenie do komunikowania się z tym typem, ale gdy tylko będziesz musiał coś z tym zrobić, tworzysz obiekt powiązany z wyliczeniem, prawdopodobnie używając Fabryki. Jest to o wiele łatwiejsze w utrzymaniu, ponieważ musisz tylko znaleźć jedno miejsce do aktualizacji: swoją Fabrykę i dodając nową klasę.
źródło
Jeśli jesteś ostrożny w sposobie ich używania, nie uważam wyliczeń za szkodliwe. Ale jest kilka rzeczy do rozważenia, jeśli chcesz użyć ich w jakimś kodzie biblioteki, w przeciwieństwie do pojedynczej aplikacji.
Nigdy nie usuwaj ani nie zmieniaj kolejności wartości. Jeśli w którymś momencie masz na liście wartość wyliczającą, wartość ta powinna być związana z tą nazwą na całą wieczność. Jeśli chcesz, możesz w pewnym momencie zmienić nazwy wartości na
deprecated_orc
cokolwiek, ale nie usuwając ich, możesz łatwiej zachować zgodność ze starym kodem skompilowanym ze starym zestawem wyliczeń. Jeśli jakiś nowy kod nie radzi sobie ze starymi stałymi wyliczania, wygeneruj odpowiedni błąd lub upewnij się, że żadna taka wartość nie osiągnie tego fragmentu kodu.Nie rób na nich arytmetyki. W szczególności nie porównuj zamówień. Dlaczego? Ponieważ wtedy możesz spotkać się z sytuacją, w której nie możesz zachować istniejących wartości i jednocześnie zachować rozsądny porządek. Weźmy na przykład wyliczenie dla kierunków kompasu: N = 0, NE = 1, E = 2, SE = 3,… Teraz, jeśli po aktualizacji wprowadzisz NNE i tak dalej między istniejącymi kierunkami, możesz dodać je na końcu listy i w ten sposób przerywa porządkowanie lub przeplata się je z istniejącymi kluczami, a tym samym łamie ustalone mapowania używane w starszym kodzie. Lub przestarzałe wszystkie stare klucze i masz zupełnie nowy zestaw kluczy, wraz z pewnym kodem kompatybilności, który tłumaczy stare i nowe klucze ze względu na starszy kod.
Wybierz odpowiedni rozmiar. Domyślnie kompilator użyje najmniejszej liczby całkowitej, która może zawierać wszystkie wartości wyliczeniowe. Co oznacza, że jeśli przy pewnej aktualizacji Twój zestaw możliwych wyliczeń powiększy się z 254 do 259, nagle potrzebujesz 2 bajtów na każdą wartość wyliczenia zamiast jednego. Może to zniszczyć strukturę i układy klas w całym miejscu, więc staraj się tego uniknąć, stosując wystarczającą wielkość w pierwszym projekcie. C ++ 11 daje tutaj dużą kontrolę, ale w przeciwnym razie podanie wpisu również
LAST_SPECIES_VALUE=65535
powinno pomóc.Posiadaj centralny rejestr. Ponieważ chcesz naprawić mapowanie między nazwami a wartościami, źle jest pozwolić innym użytkownikom kodu na dodawanie nowych stałych. Żaden projekt używający twojego kodu nie powinien mieć możliwości zmiany tego nagłówka w celu dodania nowych mapowań. Zamiast tego powinni cię zepsuć, aby je dodać. Oznacza to, że w przypadku ludzkiego i krasnoludów, o których wspomniałeś, wyliczenia są rzeczywiście kiepskie. Tam lepiej byłoby mieć jakiś rejestr w czasie wykonywania, w którym użytkownicy twojego kodu mogą wstawić ciąg znaków i uzyskać unikalny numer, ładnie opakowany w jakiś nieprzezroczysty typ. „Liczba” może nawet wskazywać na dany ciąg, to nie ma znaczenia.
Pomimo faktu, że mój ostatni punkt powyżej sprawia, że wyliczenia są źle dopasowane do twojej hipotetycznej sytuacji, twoja faktyczna sytuacja, w której niektóre specyfikacje mogą się zmienić i trzeba by zaktualizować jakiś kod, wydaje się całkiem dobrze pasować do centralnego rejestru twojej biblioteki. Zatem wyliczenia powinny być odpowiednie, jeśli weźmiecie do serca moje inne sugestie.
źródło