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
