Refaktoryzacja instrukcji zamiany i czy w ogóle istnieje rzeczywiste zastosowanie instrukcji zamiany?

28

Czytałem ten artykuł i zastanawiałem się, czy pozbyliśmy się wszystkich instrukcji zamiany, zastępując je słownikiem lub fabryką, aby w moich projektach nie było żadnych instrukcji switch.

Coś nie do końca się sumowało.

Pytanie brzmi: czy instrukcje przełączające mają jakiekolwiek zastosowanie, czy też zastępujemy je słownikiem lub metodą fabryczną (przy użyciu metody fabrycznej oczywiście do minimalnego wykorzystania instrukcji przełączających do tworzenia obiektów korzystanie z fabryki ... ale o to chodzi).

Kanini
źródło
10
Zakładając, że wdrożysz fabrykę, jak zdecydujesz, jaki typ obiektu chcesz stworzyć?
CodeART
@CodeWorks: Oczywiście będę miał gdzieś warunki, decydujące, której konkretnej implementacji użyć.
Kanini
@CodeWorks: Oczywiście z wirtualnym konstruktorem. (A jeśli nie możesz wdrożyć w ten sposób wzoru fabrycznego, potrzebujesz lepszego języka.)
Mason Wheeler

Odpowiedzi:

44

Zarówno switchoświadczenia, jak i polimorfizm mają swoje zastosowanie. Zauważ jednak, że istnieje również trzecia opcja (w językach, które obsługują wskaźniki funkcji / lambdas i funkcje wyższego rzędu): mapowanie danych identyfikatorów na funkcje obsługi. Jest to dostępne np. W C, który nie jest językiem OO, i C #, który jest *, ale nie (jeszcze) w Javie, która również jest OO *.

W niektórych językach proceduralnych (nie posiadających polimorfizmu ani funkcji wyższego rzędu) switch/ if-elseinstrukcje były jedynym sposobem rozwiązania klasy problemów. Tak wielu programistów, przyzwyczajonych do tego sposobu myślenia, nadal używało switchnawet w językach OO, w których polimorfizm jest często lepszym rozwiązaniem. Dlatego często zaleca się unikanie / refaktoryzowanie switchoświadczeń na korzyść polimorfizmu.

W każdym razie najlepsze rozwiązanie zawsze zależy od wielkości liter. Pytanie brzmi: która opcja daje czystszy, bardziej zwięzły, łatwiejszy w utrzymaniu kod na dłuższą metę?

Instrukcje Switch mogą często stać się niewygodne, mając dziesiątki przypadków, co utrudnia ich utrzymanie. Ponieważ musisz trzymać je w jednej funkcji, funkcja ta może urosnąć. W takim przypadku należy rozważyć refaktoryzację w kierunku rozwiązania opartego na mapie i / lub polimorfii.

Jeśli to samo switchzacznie pojawiać się w wielu miejscach, polimorfizm jest prawdopodobnie najlepszą opcją do ujednolicenia wszystkich tych przypadków i uproszczenia kodu. Zwłaszcza jeśli oczekuje się, że w przyszłości pojawi się więcej przypadków; im więcej miejsc trzeba aktualizować za każdym razem, tym więcej możliwości błędów. Często jednak poszczególne procedury obsługi poszczególnych przypadków są tak proste lub jest ich tak wiele lub są tak powiązane, że przefakturowanie ich do pełnej polimorficznej hierarchii klas jest nadmierne lub powoduje wiele powielonych kodów i / lub splątanie, trudno utrzymać hierarchię klas. W takim przypadku może być łatwiej użyć funkcji / lambdas (jeśli Twój język na to pozwala).

Jeśli jednak masz switchw jednym miejscu, a tylko kilka przypadków robi coś prostego, najlepszym rozwiązaniem może być pozostawienie go takim, jakim jest.

* Używam tutaj luźno terminu „OO”; Nie interesują mnie koncepcyjne debaty nad tym, co jest „prawdziwe” lub „czyste” OO.

Péter Török
źródło
4
+1. Ponadto, zalecenia te są zwykle sformułowane: „ wolą polimorfizm niż zamianę” i jest to dobry wybór słów, w dużej mierze zgodny z tą odpowiedzią. Przyznaje, że istnieją okoliczności, w których odpowiedzialny programista może dokonać innego wyboru.
Carl Manaster
1
Są chwile, kiedy chcesz zmienić podejście, nawet jeśli w kodzie jest ich miliard. Mam na myśli kawałek kodu, który generuje labirynt 3D. Wykorzystanie pamięci wzrosłoby znacznie, gdyby jednobajtowe komórki w tablicy zostały zastąpione klasami.
Loren Pechtel
14

W tym miejscu wracam do bycia dinozaurem ...

Instrukcje Switch nie są same w sobie złe, ich użycie jest kwestią sporną.

Najbardziej oczywistym z nich jest „ta sama” instrukcja przełączania powtarzana w kółko przez twój kod, który jest zły (już tam zrobiono, dołożyłbym wszelkich starań, aby nie powtórzyć tego ponownie) - i to w tym ostatnim przypadku możesz być w stanie sobie poradzić z użyciem polimorfizmu. Zwykle jest też coś dość okropnego w zagnieżdżonych skrzynkach (kiedyś miałem absolutnego potwora - nie do końca jestem pewien, jak sobie z tym radzę, poza „lepszym”).

Słownik jako przełącznik Uważam, że jest trudniejszy - zasadniczo tak, jeśli twój przełącznik obejmuje 100% przypadków, ale tam, gdzie chcesz mieć domyślne przypadki braku akcji lub akcji, zaczyna być nieco bardziej interesujący.

Myślę, że chodzi o unikanie powtórzeń i upewnienie się, że tworzymy nasze wykresy obiektowe we właściwych miejscach.

Ale jest też argument zrozumienia (łatwości konserwacji), który ogranicza obie strony - gdy zrozumiesz, jak to wszystko działa (wzorzec i aplikacja, w której jest zaimplementowany), jest to łatwe ... ale jeśli dojdziesz do jednego wiersza kodu, w którym musisz dodać coś nowego, a następnie przeskoczyć w to miejsce, aby ustalić, co musisz dodać / zmienić.

Pomimo tego, że nasze środowiska programistyczne są ogromnie zdolne, nadal uważam, że pożądane jest, aby móc zrozumieć kod (tak jakby to był) wydrukowany na papierze - czy możesz śledzić kod palcem? Akceptuję to w rzeczywistości nie, przy wielu dobrych praktykach dzisiaj nie możesz i z dobrych powodów, ale to oznacza, że ​​trudniej jest zacząć od kodowania (a może jestem po prostu stary ...)

Murph
źródło
4
Miałem nauczyciela, który powiedział, że najlepiej napisać kod „aby ktoś, kto nic nie wiedział o programowaniu (jak być może twoja matka), mógł go zrozumieć”. Niestety jest to ideał, ale może i tak powinniśmy uwzględnić nasze mamy w recenzjach kodu!
Michael K
+1 za „ale jeśli dojdziesz do jednego wiersza kodu, w którym musisz dodać coś nowego, musisz skakać po całym miejscu, aby dowiedzieć się, co musisz dodać / zmienić”
szybko_nie
8

Stwierdzenia zamiany kontra polimorfizm podtypów to stary problem i często jest przywoływany w społeczności FP w dyskusjach na temat problemu ekspresji .

Zasadniczo mamy typy (klasy) i funkcje (metody). Jak kodujemy rzeczy, aby można było łatwo dodawać nowe typy lub nowe metody?

Jeśli programujesz w stylu OO, trudno jest dodać nową metodę (ponieważ oznaczałoby to refaktoryzację wszystkich istniejących klas), ale bardzo łatwo jest dodać nowe klasy, które używają takich samych metod jak poprzednio.

Z drugiej strony, jeśli użyjesz instrukcji switch (lub jej odpowiednika OO, wzorca obserwatora), wówczas bardzo łatwo będzie dodać nowe funkcje, ale trudno będzie dodać nowe przypadki / klasy.

Nie jest łatwo mieć dobrą rozszerzalność w obu kierunkach, więc pisząc kod, określ, czy chcesz użyć polimorfizmu, czy przełączaj instrukcje, w zależności od tego, w którym kierunku będziesz bardziej skłonny je rozszerzyć.

hugomg
źródło
4
pozbywamy się wszystkich instrukcji zamiany, zastępując je słownikiem lub fabryką, aby w moich projektach nie było żadnych instrukcji switch.

Nie. Takie absoluty rzadko są dobrym pomysłem.

W wielu miejscach słownik / wyszukiwanie / fabryka / metoda polimorficzna zapewnia lepszy projekt niż instrukcja switch, ale nadal trzeba wypełnić słownik. W niektórych przypadkach może to zaciemnić to, co się faktycznie dzieje, a zwykłe umieszczenie instrukcji switch w wierszu jest bardziej czytelne i łatwiejsze w utrzymaniu.

Telastyn
źródło
I tak naprawdę, jeśli rozwinąłbyś fabrykę, słownik i wyszukiwanie, okazałoby się, że jest to w zasadzie instrukcja switch: jeśli tablica [hash (szukaj)], to wywołaj tablicę [hash (szukaj)]. Chociaż skompilowane przełączniki nie są rozszerzalne w czasie wykonywania.
Zan Lynx
@ZanLynx, całkowite przeciwieństwo jest prawdziwe, przynajmniej w języku C #. Jeśli spojrzysz na IL utworzoną dla nietrywialnej instrukcji switch, zobaczysz, że kompilator zamienia ją w słownik. Ponieważ jest szybszy.
David Arno,
@DavidArno: „kompletne przeciwieństwo”? Sposób, w jaki go czytałem, powiedzieliśmy dokładnie to samo. Jak może być odwrotnie?
Zan Lynx,
2

Zważywszy na to, że jest to niezależne od języka, przejściowy kod najlepiej działa z switchinstrukcjami:

switch(something) {
   case 1:
      foo();
   case 2:
      bar();
      baz();
      break;

   case 3:
      bang();
   default:
      bizzap();
      break;
}

Równoważne z if, z bardzo niewygodnej przypadku domyślnym. I pamiętaj, że im więcej wpadnie, tym dłużej lista warunków dostanie:

if (1 == something) {
   foo();
}
if (1 == something || 2 == something) {
   bar();
   baz();
}
if (3 == something) {
   bang();
}
if (1 != something && 2 != something) {
   bizzap();
}

(Biorąc jednak pod uwagę to, co mówią niektóre inne odpowiedzi, wydaje mi się, że nie trafiłem w sedno pytania ...)

Izkata
źródło
Uznałbym, że użycie fałszywych stwierdzeń na poziomie switcha jest takie samo goto. Jeśli algorytm, który ma zostać wykonany, wymaga przepływów sterowania, które pasują do strukturalnych struktur programowania, należy użyć takich konstrukcji, ale jeśli algorytm nie pasuje do takich konstrukcji, użycie gotomoże być lepsze niż próba dodania flag lub innej logiki sterowania w celu dopasowania do innych struktur kontroli .
supercat
1

Instrukcje przełączające, ponieważ rozróżnianie wielkości liter ma rzeczywiste zastosowanie. Funkcjonalne języki programowania używają czegoś, co nazywa się dopasowywaniem wzorców, co pozwala definiować funkcje w różny sposób w zależności od wprowadzanych danych. Jednak w językach obiektowych ten sam cel można osiągnąć bardziej elegancko, stosując polimorfizm. Po prostu wywołujesz metodę, a w zależności od faktycznego typu obiektu zostanie wykonana odpowiednia implementacja metody. To jest przyczyna tego, że instrukcje switch mają zapach kodu. Jednak nawet w językach OO mogą okazać się przydatne do implementacji abstrakcyjnego wzorca fabrycznego.

tl; dr - są przydatne, ale dość często można to zrobić lepiej.

szalik
źródło
2
Czuję tam trochę stronniczości - dlaczego polimorfizm jest bardziej elegancki niż dopasowanie wzoru? A co sprawia, że ​​myślisz, że polimorfizm nie istnieje w FP?
tdammers
@tdammers Po pierwsze nie chciałem sugerować, że polimorfizm nie istnieje w FP. Haskell obsługuje polimorfizm ad hoc i parametryczny. Dopasowanie wzorca ma sens dla algebraicznego typu danych. Ale w przypadku modułów zewnętrznych wymaga to ujawnienia konstruktorów. Wyraźnie przerywa to enkapsulację abstrakcyjnego typu danych (realizowanego za pomocą algebraicznych typów danych). Dlatego uważam, że polimorfizm jest bardziej elegancki (i możliwy w FP).
szalik
Zapominacie o polimorfizmie w typach i instancjach.
tdammers
Czy kiedykolwiek słyszałeś o problemie z wyrażaniem ? OO i FP są lepsze w różnych sytuacjach, jeśli przestaniesz o tym myśleć.
hugomg
@tdammers Jak myślisz, o jakim polimorfizmie mówiłem, kiedy wspomniałem o polimorfizmie ad hoc? Polimorfizm ad hoc realizowany jest w Haskell poprzez klasy typów. Jak możesz powiedzieć, że zapomniałem? Po prostu nieprawda.
scarfridge