Dylemat
Czytałem wiele książek o najlepszych praktykach na temat praktyk zorientowanych obiektowo, a prawie każda książka, którą przeczytałem, miała tę część, w której mówiły, że enumy to zapach kodu. Myślę, że przegapili tę część, w której wyjaśniają, kiedy wyliczenia są ważne.
Jako taki szukam wytycznych i / lub przypadków użycia, w których wyliczenia NIE są zapachem kodu, a właściwie prawidłową konstrukcją.
Źródła:
„OSTRZEŻENIE Zazwyczaj wyliczenia są zapachami kodowymi i powinny zostać przekształcone w klasy polimorficzne. [8]” Seemann, Mark, Dependency Injection w .Net, 2011, s. 1. 342
[8] Martin Fowler i in., Refactoring: Improving the Design of Existing Code (New York: Addison-Wesley, 1999), 82.
Kontekst
Przyczyną mojego dylematu jest API transakcyjne. Dają mi strumień danych Tick przesyłając tę metodę:
void TickPrice(TickType tickType, double value)
gdzie enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }
Próbowałem utworzyć opakowanie wokół tego interfejsu API, ponieważ łamanie zmian jest sposobem na życie dla tego interfejsu API. Chciałem śledzić wartość każdego ostatnio otrzymanego typu kleszcza na moim opakowaniu i zrobiłem to, korzystając ze słownika typów kleszczy:
Dictionary<TickType,double> LastValues
Wydawało mi się, że to właściwe użycie wyliczenia, jeśli są one używane jako klucze. Ale mam inne przemyślenia, ponieważ mam miejsce, w którym podejmuję decyzję na podstawie tej kolekcji i nie mogę wymyślić sposobu, w jaki sposób mogę wyeliminować instrukcję zmiany, mógłbym użyć fabryki, ale ta fabryka nadal będzie miała gdzieś instrukcja switch. Wydawało mi się, że po prostu się poruszam, ale wciąż pachnie.
Łatwo jest znaleźć DON'T wyliczeń, ale DO, nie takie łatwe, i byłbym wdzięczny, gdyby ludzie mogli dzielić się swoją wiedzą, zaletami i wadami.
Namysł
Niektóre decyzje i działania opierają się na nich TickType
i wydaje mi się, że nie mogę wymyślić sposobu na wyeliminowanie instrukcji enum / switch. Najczystszym rozwiązaniem, jakie mogę wymyślić, jest użycie fabryki i zwrot implementacji na podstawie TickType
. Nawet wtedy nadal będę mieć instrukcję switch, która zwraca implementację interfejsu.
Poniżej wymieniono jedną z przykładowych klas, w których mam wątpliwości, że mógłbym źle użyć wyliczenia:
public class ExecutionSimulator
{
Dictionary<TickType, double> LastReceived;
void ProcessTick(TickType tickType, double value)
{
//Store Last Received TickType value
LastReceived[tickType] = value;
//Perform Order matching only on specific TickTypes
switch(tickType)
{
case BidPrice:
case BidSize:
MatchSellOrders();
break;
case AskPrice:
case AskSize:
MatchBuyOrders();
break;
}
}
}
enums as switch statements might be a code smell ...
Odpowiedzi:
Wyliczenia są przeznaczone do użycia, gdy dosłownie wyliczyłeś każdą możliwą wartość, jaką może przyjąć zmienna. Zawsze. Pomyśl o przypadkach użycia, takich jak dni tygodnia lub miesiące w roku lub wartości konfiguracji rejestru sprzętu. Rzeczy, które są zarówno bardzo stabilne, jak i reprezentowalne przez prostą wartość.
Pamiętaj, że jeśli tworzysz warstwę antykorupcyjną, nie możesz uniknąć gdzieś instrukcji switch ze względu na projekt, który pakujesz, ale jeśli zrobisz to dobrze, możesz ograniczyć ją do tego jednego miejsca i użyj polimorfizmu w innym miejscu.
źródło
Po pierwsze, zapach kodu nie oznacza, że coś jest nie tak. Oznacza to, że coś może być nie tak.
enum
pachnie, ponieważ jest często nadużywane, ale to nie znaczy, że musisz ich unikać. Wystarczy, że zaczniesz pisaćenum
, zatrzymasz się i zaczniesz szukać lepszych rozwiązań.Szczególny przypadek, który ma miejsce najczęściej, występuje wtedy, gdy różne wyliczenia odpowiadają różnym typom o różnych zachowaniach, ale z tym samym interfejsem. Na przykład rozmawianie z różnymi backendami, renderowanie różnych stron itp. Są one o wiele bardziej naturalnie realizowane za pomocą klas polimorficznych.
W twoim przypadku TickType nie odpowiada różnym zachowaniom. Są to różne typy zdarzeń lub różne właściwości bieżącego stanu. Myślę więc, że jest to idealne miejsce na wyliczenie.
źródło
Podczas przesyłania danych wyliczenia nie mają zapachu kodu
IMHO, przy przesyłaniu danych za pomocą
enums
wskazania, że pole może mieć wartość z ograniczonego (rzadko zmieniającego się) zestawu wartości, jest dobre.Uważam, że lepiej jest przekazywać dowolnie
strings
lubints
. Ciągi mogą powodować problemy z powodu zmian pisowni i wielkich liter. Ints pozwalają na transmisję z wartościami wielkości i mają małe semantykę (np otrzymam3
od usługi handlu, co to znaczy?LastPrice
?LastQuantity
? Coś jeszcze?Przesyłanie obiektów i używanie hierarchii klas nie zawsze jest możliwe; na przykład wcf nie pozwala odbiorcy na rozróżnienie, która klasa została wysłana.
Innym powodem, dla którego nie chcemy przesyłać obiektów klasy, jest to, że usługa może wymagać zupełnie innego zachowania dla przesyłanego obiektu (np.
LastPrice
) Niż klient. W takim przypadku wysyłanie klasy i jej metod jest niepożądane.Czy instrukcje przełączników są złe?
IMHO, pojedyncza informacja,
switch
która wywołuje różne konstruktory w zależności od,enum
nie jest zapachem kodu. Niekoniecznie jest lepsza lub gorsza od innych metod, takich jak refleksja oparta na nazwie typu; zależy to od faktycznej sytuacji.Po włączeniu przełączników
enum
wszędzie jest zapach kodu, oop zapewnia alternatywy, które są często lepsze:źródło
int
. Moje opakowanie jest tym, które używa,TickType
które jest rzutowane z otrzymanegoint
. Kilka zdarzeń wykorzystuje ten typ tygrysa z dziko zmieniającymi się podpisami, które są odpowiedziami na różne żądania. Czy powszechną praktyką jestint
ciągłe używanie do różnych funkcji? np.TickPrice(int type, double value)
używa 1,3 i 6,type
podczas gdyTickSize(int type, double value
używa 2,4 i 5? Czy sens ma nawet rozdzielenie ich na dwa zdarzenia?co jeśli wybierzesz bardziej złożony typ:
wtedy możesz załadować swoje typy z refleksji lub sam je zbudować, ale najważniejsze jest to, że trzymasz się zasady otwartego zamykania SOLID
źródło
TL; DR
Aby odpowiedzieć na pytanie, mam teraz trudności z myśleniem, że wyliczenia czasu nie są zapachem kodu na pewnym poziomie. Istnieje pewna intencja, aby deklarowali efektywnie (istnieje wyraźnie ograniczona, ograniczona liczba możliwości dla tej wartości), ale ich z natury zamknięty charakter czyni je gorszymi architektonicznie.
Przepraszam, gdy zmieniam tonę mojego starszego kodu. / westchnienie; ^ D
Ale dlaczego?
Całkiem niezła recenzja, dlaczego tak jest w przypadku LosTechies tutaj :
Kolejny przypadek wdrożenia ...
Najważniejsze jest to, że jeśli masz zachowanie zależne od wartości wyliczeniowych, dlaczego zamiast tego nie masz różnych implementacji podobnego interfejsu lub klasy nadrzędnej, które zapewniają, że ta wartość istnieje? W moim przypadku patrzę na różne komunikaty o błędach w oparciu o kody stanu REST. Zamiast...
... może powinienem zawrzeć kody statusu w klasach.
Następnie za każdym razem, gdy potrzebuję nowego typu IAmStatusResult, koduję go ...
... a teraz mogę upewnić się, że wcześniejszy kod zdaje sobie sprawę, że ma
IAmStatusResult
zasięg i odwołuje się do niegoentity.ErrorKey
zamiast do bardziej skomplikowanego, deadend_getErrorCode(403)
.I, co ważniejsze, za każdym razem, gdy dodam nowy typ wartości zwracanej, nie trzeba dodawać żadnego innego kodu, aby ją obsłużyć . Co wiesz,
enum
iswitch
prawdopodobnie były to zapachy kodowe.Zysk.
źródło
StatusResult
wartości? Można argumentować, że wyliczanie jest przydatne jako niezapomniany dla człowieka skrót w tym przypadku użycia, ale prawdopodobnie nadal nazwałbym to zapachem kodu, ponieważ istnieją dobre alternatywy, które nie wymagają zamkniętego zbioru.enum
, to nie trzeba kodu aktualizacji za każdym razem dodać lub usunąć wartość i potencjalnie wiele miejsc, w zależności od tego, jak skonstruowany kod. Używanie polimorfizmu zamiast wyliczenia jest trochę jak upewnienie się, że kod jest w normalnej formie Boyce-Codda .To, czy użycie wyliczenia jest zapachem kodu, zależy od kontekstu. Myślę, że możesz znaleźć pomysły na odpowiedź na twoje pytanie, jeśli weźmiesz pod uwagę problem z ekspresją . Tak więc masz kolekcję różnych typów i zestaw operacji na nich, i musisz uporządkować swój kod. Istnieją dwie proste opcje:
Które rozwiązanie jest lepsze?
Jeśli, jak wskazał Karl Bielefeldt, twoje typy są stałe i oczekujesz, że system będzie się rozwijał głównie poprzez dodawanie nowych operacji na tych typach, wówczas użycie wyliczenia i posiadanie instrukcji switch jest lepszym rozwiązaniem: za każdym razem, gdy potrzebujesz nowej operacji, po prostu zaimplementuj nową procedurę, podczas gdy za pomocą klas będziesz musiał dodać metodę do każdej klasy.
Z drugiej strony, jeśli oczekujesz raczej stabilnego zestawu operacji, ale uważasz, że z czasem będziesz musiał dodać więcej typów danych, korzystanie z rozwiązania obiektowego jest wygodniejsze: ponieważ nowe typy danych muszą zostać zaimplementowane, po prostu dodawaj nowe klasy implementujące ten sam interfejs, natomiast jeśli korzystasz z wyliczenia, musisz zaktualizować wszystkie instrukcje switch we wszystkich procedurach korzystających z wyliczenia.
Jeśli nie możesz sklasyfikować swojego problemu w żadnej z dwóch powyższych opcji, możesz spojrzeć na bardziej wyrafinowane rozwiązania (patrz np. Ponownie na cytowaną powyżej stronę Wikipedii w celu krótkiej dyskusji i odniesienia do dalszego czytania).
Powinieneś więc spróbować zrozumieć, w jakim kierunku może ewoluować Twoja aplikacja, a następnie wybrać odpowiednie rozwiązanie.
Ponieważ książki, na które się powołujesz, dotyczą paradygmatu zorientowanego obiektowo, nic dziwnego, że są one stronnicze w stosunku do używania wyliczeń. Jednak rozwiązanie zorientowane obiektowo nie zawsze jest najlepszą opcją.
Konkluzja: wyliczenia niekoniecznie są zapachem kodu.
źródło
paint()
,show()
,close()
resize()
operacje i niestandardowe widgety) to podejście zorientowane obiektowo jest lepszy w tym sensie, że pozwala na dodanie nowego typu bez wpływu zbyt dużo istniejącego kodu (w zasadzie implementujesz nową klasę, która jest lokalną zmianą).