Switch Statements

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)?

  1. Jeśli warunki dotyczą stanu obiektu możliwe, że warto zastosować wzorzec Stan [State].
  2. 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.
  3. 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].
  4. 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?

W mojej opinii w sytuacji, gdy logika nie jest skomplikowana i się nie powtarza, kombinowanie, aby to modyfikować może okazać się sztuką dla sztuki. Dodatkowo ciężko byłoby zastosować wzorzec Metoda Wytwórcza bądź Fabryka Abstrakcyjna [Abstract Factory] bez użycia switch (bądź if).

Uwagi końcowe

  1. 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.
  2. Jeśli przemawia do Ciebie bardziej wersja wideo to możesz obejrzeć ten filmik z YouTube Replace Conditional With Polymorphism

Źródła

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.