28-09-2022
Instrukcje switch* same w sobie nie są złe, jednakże są sytuacje w których można podejść do sytuacji odrobinę inaczej. „Switch Statements” jest kolejnym brzydkim zapaszkiem kodu. Określeniem brzydki zapaszek nazywane są miejsca w kodzie, o których warto przemyśleć ich modyfikację. Fragmenty kodu, które można napisać inaczej, lepiej. Więcej o tym możesz znaleźć na mojej stronie Code Smells. Tam opisuję to pojęcie oraz podaję listę zapaszków – bo na koniec dnia Switch Statements nie jest jedynym zapaszkiem 😉
*Będę się posługiwać tylko nazwą switch, ale na myśli mam również ich odpowiednik if else o tym samym zastosowaniu.
Tak jak już zaznaczyłam na początku samo używanie switch nie jest złe. Jest czasami złe. Więc skąd masz wiedzieć kiedy jest złe?
Powtórzona SWITCHologia
Jeżeli w więcej niż jednym miejscu masz zastosowane dokładnie te same warunki to znak, że warto pomyśleć o innym podejściu. Wyobraź sobie sytuację w której zmiany musisz wprowadzać zawsze w 2 miejscach. Jest to problematyczne, a z czasem może dojść do sytuacji, że zapomnisz o dodaniu do SWITCHa tego samego warunku.
Skomplikowana logika
W momencie gdy logika w switch jest przerośnięta, trudna do zrozumienia, również warto pomyśleć o modyfikacji.
Jak podejść do powyższych tematów (powtórzenia / skomplikowana logika)?
- Jeśli warunki dotyczą stanu obiektu możliwe, że warto zastosować wzorzec Stan [State].
- Jeśli mamy te same warunki w kilku miejscach, warto rozważyć wyniesienie do wspólnego, jednego miejsca w kodzie (nowa metoda, a może i nawet nowa klasa – w zależności od sytuacji). Dzięki temu kolejne warunki będziesz dodawać w jednym miejscu.
- Spójrz czy możesz wydzielić logiki do osobnych klas i pomyśleć o wspólnym interfejsie (zastosowanie wzorca Strategia [Strategy]). Do tworzenia obiektów możesz użyć wzorca Metoda Wytwórcza [Factory Method].
- Jeśli powyższe punkty nie rozwiązują Twojej konkretnej sytuacji, spójrz czy zastosowanie wzorca Polecenie [Command] jest Twoim rozwiązaniem 🙂
Specjalizacja
Punkt ten może brzmieć enigmatycznie, ale wydaje mi się, że w tym przypadku najlepiej zacząć od przykładu zanim omówimy sobie o co chodzi w tej sytuacji. Poniżej mamy przykład zapaszku Switch Statements w klasie ProgressBar. Posiada ona metodę setValue i otrzymuje zmienną $type oraz $value. W zależności od przekazanego typu następuje wykonanie pewnej logiki, która dotyczy tej klasy.
final class ProgressBar { private int $barWidth = 28; private string $barChar; private ?string $format = null; private ?string $internalFormat = null; private ?int $max = null; private int $stepWidth; // ..... public function setValue(string $type, $value): void { switch ($type) { case 'width': $this->barWidth = max(1, $value); break; case 'char': $this->barChar = $char; break; case 'format': $this->format = null; $this->internalFormat = $format; break; case 'max_steps': $this->format = null; $this->max = max(0, $max); $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; break; default: throw new \InvalidArgumentException("Unknown type"); } }
Do metody setValue, można podejść inaczej. Jak? Można przekazać parametry przez konstruktor, ale jeśli zależy nam na ustawieniu wartości później z jakiegoś powodu, to możemy wyspecjalizować metody. Tworzymy metody odpowiedzialne za dane działanie, dzięki czemu pozbywamy się w tym miejscu instrukcji switch i przekazywania zmiennej $type. Dodatkowo możemy dodać oczekiwany typ zmiennej $value, która czasami przyjmuje wartości tekstowe, a kiedy indziej jest to liczba. Poniżej możesz zobaczyć kod już bez brzydkiego zapaszku. Poniższy przykład jest fragmentem klasy z frameworka Symfony (pełną klasę możesz zobaczyć tutaj). Kod wyżej jest moją interpretacją złego zapaszku na potrzeby tego wpisu.
final class ProgressBar { private int $barWidth = 28; private string $barChar; private ?string $format = null; private ?string $internalFormat = null; private ?int $max = null; private int $stepWidth; // .... public function setBarWidth(int $size) { $this->barWidth = max(1, $size); } public function setBarCharacter(string $char) { $this->barChar = $char; } public function setFormat(string $format) { $this->format = null; $this->internalFormat = $format; } public function setMaxSteps(int $max) { $this->format = null; $this->max = max(0, $max); $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; } }
Kiedy nie warto ruszać switch?
Uwagi końcowe
- W swojej książce Refaktoryzacja. Ulepszanie struktury istniejącego kodu, autorzy w 2 wydaniu książki zauważają, że obecnie widzą ten problem jako mniej dominujący. Zwracają uwagę, że kiedyś programiści nie doceniali polimorfizmu, co obecnie się już zmieniło. Autorzy z tego powodu w 2 wydaniu zmienili swoje podejście do instrukcji warunkowych i potępiają głównie tylko powtórzone instrukcje warunkowe.
- Jeśli przemawia do Ciebie bardziej wersja wideo to możesz obejrzeć ten filmik z YouTube Replace Conditional With Polymorphism
Źródła
- książka: Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler we współpracy z Kentem Beckiem
- książka: Refaktoryzacja do wzorców projektowych, Joshua Kerievsky
- www: Bad Smells in Software – a Taxonomy and an Empirical Study, Mika Mäntylä
- www: Switch Statements, Aleksander Shvets
- www: Symfony GitHub