You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.

Loading...

Blog

Zde najdete zajímavé články ze světa IT.
Chcete napsat článek? Zapojte se.

(Ne)používejte switche!!!

zpět na kategorii Programátorské tipy a triky - Před 2 lety, Autor: Antonín Jehlář


Je na čase si vysvětlit, proč bychom měli použití switchů omezit na minimum, případně je úplně vynechat.



Za celých 18 let, co programuju jsem switch použil 2x. Jednou ve škole a podruhé, když jsem se nudil a chtěl vyzkoušet něco exotického.

Tak si přečti tento článek a zjisti, jak se jich taky zbavit. V první části článku si vyjmenujeme typické příklady použití SWITCHŮ a v druhé si ukážeme, jak je nahradit jinými konstrukcemi, aby byl kód čitelnější, čistčí a rozšířitelnější.

TL;DR? koukni na video:

Příklady použití switchů.

Všechny příklady jsou v PHP, ale obecně to platí pro všechny ostatní jazyky

1. Switchování výrazů

Prvně bychom si měli říci, že SWITCH konstrukce se liší od IF konstrukcí tím, že IF porovnává vůči rozsahům a SWITCH vůči konstantám.
Některé jazyky podporují u SWITCHů dokonce kromě konstant i výrazy. Takové použití je ale dost mimo, protože jak jsem psal, tak na výrazy zde jsou IFy, které již umíme krásně jednoduše zapsat díky článku o nepoužívání else statementů.
Navíc takové používání SWITCHE může vést k výkonnostním problémům, jelikož switch na výrazy a porovnání není dělaný a tedy se poté buď simuluje jako série IFů, nebo se prostě snaží ten SWITCH rozšířit o všechny případy (jelikož SWITCH není nic jiného, než skoky v paměti na konstantou předem určené místo), což může aplikaci mnohonásobně zpomalit.

const
    SUNDAY = 0,
    MONDAY = 1,
    TUESDAY = 2,
    WEDNESDAY = 3,
    THURSDAY = 4,
    FRIDAY = 5,
    SATURDAY = 6;

public function getWeekPart(int $day): string
{
    switch (true) {
        case ($day > self::SUNDAY && $day < self::SATURDAY):
            $retVal = 'weekday';
            break;
        case ($day === self::SUNDAY || $day === self::SATURDAY):
            $retVal = 'weekend';
            break;
        default:
            $retVal = 'Not from this planet';
            break;
    }
    
    return $retVal;
}

Pro porovnávání výrazů (range, větší, menší a více hodnot) tedy používej IFy a nikdy ne SWITCHE!!!

2. Výběr konstant

Druhý příklad se týká toho, kdy potřebujeme mít v rámci aplikace nadefinované konstanty, ale ty poté chceme překládat například na uživatelské hlášky. Toto je už z podstaty nesmysl, jelikož chceme pomocí SWITCHE překládat jedny konstanty na jiné konstanty.

const
    ALL_OK = 0,
    TOO_MANY_ARGUMENTS = 1,
    MISSING_ARGUMENT = 2,
    ZERO_DIVISIBILITY = 3;

public function getErrorMessage(int $errorCode): string
{
    switch ($errorCode) {
        case self::ALL_OK:
            $retVal = 'Everything is OK';
            break;
        case self::TOO_MANY_ARGUMENTS:
            $retVal = 'You have provided too many arguments';
            break;
        case self::MISSING_ARGUMENT:
            $retVal = 'One or more arguments are missing';
            break;
        case self::ZERO_DIVISIBILITY:
            $retVal = 'You divided by zero.. REALLY?!?!?';
            break;
        default:
            $retVal = 'Unexpected error';
            break;
    }
    
    return $retVal;
}

3. Volání funkcí

Taktéž se SWITCHE používají v mnoha případech na volání konkrétní funkce z obecné funkce.

const
    CIRCLE_CONTENT = 1,
    RECTANGLE_CONTENT = 2,
    BLOCK_CONTENT = 3;

public function countContent(...$sites): float
{
    $sitesCount = count($sites);

    switch ($sitesCount) {
        case self::CIRCLE_CONTENT:
            $retVal = $this->countCircleContent(...$sites);
            break;
        case self::RECTANGLE_CONTENT:
            $retVal = $this->countRectangleContent(...$sites);
            break;
        case self::BLOCK_CONTENT:
            $retVal = $this->countBlockContent(...$sites);
            break;
        default:
            $retVal = 0;    // or THROW?
            break;
    }
    
    return $retVal;
}

private function countCircleContent(int $siteA): float
{
    return $siteA * $siteA * pi();
}

private function countRectangleContent(int $siteA, int $siteB): float
{
    return $siteA * $siteB;
}

private function countBlockContent(int $siteA, int $siteB, int $siteC): float
{
    return $siteA * $siteB * $siteC;
}

4. Tvorba/výběr správné instance třídy pro další práci

Dále se SWITCH zneužívá na tvorbu nových instancí, které jsou rozdílné pro různé případy, které rozlišujeme konstantami. Případně na výběr správné instanci z již vytvořených instancí...

const
    MYSQL_DRIVER = 1,
    PGSQL_DRIVER = 2,
    SQLITE_DRIVER = 3;

public function createDriver(int $driverConstant): SQLDriverInterface
{
    switch ($driverConstant) {
        case self::MYSQL_DRIVER:
            $retVal = new MySQLDriver();
            break;
        case self::PGSQL_DRIVER:
            $retVal = new PgSQLDriver();
            break;
        case self::SQLITE_DRIVER:
            $retVal = new SQLLiteDriver();
            break;
        default:
            THROW ...
    }
    
    return $retVal;
}

5. Vyhodnocení příznaků pro změnu chování

No a v neposlední řadě se SWITCH často používá na změnu chování třídy na základě nějakého příznaku, který je taky v rámci konstanty, což je asi nejhorší možný případ, jak switch použít...

class Vehicle {
    public const
        PLANE = 1,
        CAR = 2,
        BOAT = 3;
        
    public function __construct(
        private readonly int $type,
    ) { }
    
    public function move(): void
    {
        switch ($this->type) {
            case self::PLANE:
                echo 'I\'m flying';
                break;
            case self::CAR:
                echo 'I\'m going';
                break;
            case self::BOAT:
                echo 'I\'m swimming';
                break;
            default:
                THROW ...
        }
    }
    
    public function refill(): void
    {
        switch ($this->type) {
            case self::PLANE:
                echo 'Avitation filled';
                break;
            case self::CAR:
                echo 'Diesel filled';
                break;
            case self::BOAT:
                echo 'Coal filled';
                break;
            default:
                THROW ...
        }
    }
}

Jestli jsi dočetl/a až sem a říkáš si, že na tom použití přeci není nic špatně, tak čti dále. Začíná část 2.

Teď si ukážeme, jak jednotlivé případy řešit bez hnusného switche, čistěji a efektivněji

1. Switchování výrazů

Jak už jsem psal, na toto je nutné použít IFy. Je to lepší..

const
    SUNDAY = 0,
    MONDAY = 1,
    TUESDAY = 2,
    WEDNESDAY = 3,
    THURSDAY = 4,
    FRIDAY = 5,
    SATURDAY = 6;

public function getWeekPart(int $day): string
{
    if ($day > self::SUNDAY && $day < self::SATURDAY) return 'weekday';
    if ($day === self::SUNDAY || $day === self::SATURDAY) return 'weekend';
    return 'Not from this planet';
}

Easy peasy

2. Výběr konstant

Překlad konstant na konstanty? Vážně? Tak máme 2 řešení. Buď použijeme v konstantách přímo hodnoty, které potřebujeme, nebo pokud potřebujeme jednu konstantu překládat na více hodnot, můžeme udělat pole, kam budou konstanty přistupovat. To nám zpřehlední kód, jelikož budeme mít všechny hlášky na jednom místě a zjednoduší rozšířitelnost, protože místo úprav SWITCHů prostě jen přidáme prvek do pole, které je definované na začátku souboru/třídy a nemusíme hledat, který switch změnit a kde... Funkce zůstanou obecné a nebudeme do nich muset sahat.

const
    ALL_OK = 0,
    TOO_MANY_ARGUMENTS = 1,
    MISSING_ARGUMENT = 2,
    ZERO_DIVISIBILITY = 3,
    
    ERROR_MESSAGES = [
        self::ALL_OK             => 'Everything is OK',
        self::TOO_MANY_ARGUMENTS => 'You have provided too many arguments',
        self::MISSING_ARGUMENT   => 'One or more arguments are missing',
        self::ZERO_DIVISIBILITY  => 'You divided by zero.. REALLY?!?!?',
    ];

public function getErrorMessage(int $errorCode): string
{
    return self::ERROR_MESSAGES[$errorCode] ?? 'Unexpected error';
}

3. Volání funkcí

Tady se taky nabízí hned 2 řešení. UŽ MÁŠ KONKRÉTNÍ FUNKCE, TAK PROSTĚ POUŽIJ TY!!! A když to z nějakého důvodu nejde.. Slyšel/a jsi o věci zvané CALLBACK? 😆

const
    CIRCLE_CONTENT = 1,
    RECTANGLE_CONTENT = 2,
    BLOCK_CONTENT = 3;

private readonly array $callbacks;

public function __construct()
{
    $this->callbacks = [
        self::CIRCLE_CONTENT        => $this->countCircleContent(...),
        self::RECTANGLE_CONTENT     => $this->countRectangleContent(...),
        self::BLOCK_CONTENT         => $this->countBlockContent(...),
    ];
}

public function countContent(...$sites): float
{
    $sitesCount = count($sites);

    $callback = $this->callbacks[$sitesCount] ?? null;

    if ($callback === null) {
        return 0;
    }

    return ($callback)(...$sites);
}

private function countCircleContent(int $siteA): float
{
    return $siteA * $siteA * pi();
}

private function countRectangleContent(int $siteA, int $siteB): float
{
    return $siteA * $siteB;
}

private function countBlockContent(int $siteA, int $siteB, int $siteC): float
{
    return $siteA * $siteB * $siteC;
}

Callbacky musely být v proměnné a plněné například konstruktorem, jelikož PHP ještě nepodporuje výrazy v deklaraci třídních proměnných/konstant.

4. Tvorba/výběr správné instance třídy pro další práci

Podobný případ jako bod 3, jen s tím rozdílem, že buď máme pole tříd implementujích stejný interface, nebo pole názvů tříd, které chceme tvořit:

const
    MYSQL_DRIVER = 1,
    PGSQL_DRIVER = 2,
    SQLITE_DRIVER = 3,
    
    ALLOWED_DRIVERS = [
        self::MYSQL_DRIVER  => MySQLDriver::class,
        self::PGSQL_DRIVER  => PgSQLDriver::class,
        self::SQLITE_DRIVER => SQLLiteDriver::class,
    ];

public function createDriver(int $driverConstant): SQLDriverInterface
{
    $class = self::ALLOWED_DRIVERS[$driverConstant] ?? THROW ...;
    
    $instance = new $class();
    assert($instance instanceof SQLDriverInterface);
    
    return $retVal;
}

V případě, že přibyde nová instance na tvoření, tak jednoduše přidáme instanci do pole. Doporučuji ale jak v původním příkladu, tak i v tomto řešení místo tříd, co se mají tvořit a slovček new používat návrhový vzor Factory a v daném poli mít ty továrny/accessory, kvůli dalším závislostem a DI, který je vstříkne.

5. Vyhodnocení příznaků pro změnu chování

Peklo. Jiným slovem se to popsat nedá. Pokud tohle někdy uděláte, utrhněte si uši. Co když takto budu chování ověřovat switchem ne na 2 místech, jako v příkladu, ale na třeba 10 místech a nedej bůh v různých třídách?

Však co? Řekneš si. No jo, ale co když pak musím přidat dopravní prostředek, což bude třeba TELEPORT? Pak musíš hledat, kde všude existuje takový switch a kde všude to musíš přidat a musíš upravovat 10 míst.

Radši definuj chování pomocí konkrétních tříd:

interface Vehicle
{
    public function move(): void;
    public function refill(): void;
}

class Plane implements Vehicle 
{
    public function move(): void
    {
        echo 'I\'m flying';
    }
    
    public function refill(): void
    {
        echo 'Avitation filled';
    }
}

class Car implements Vehicle 
{
    public function move(): void
    {
        echo 'I\'m going';
    }
    
    public function refill(): void
    {
        echo 'Diesel filled';
    }
}

class Boat implements Vehicle 
{
    public function move(): void
    {
        echo 'I\'m swimming';
    }
    
    public function refill(): void
    {
        echo 'Coal filled';
    }
}

A tvoř jejich instance pomocí příkladů výše. Může se ti to zdát jako zdlouhavé psaní více souborů, tříd, továren atp. Ale věř, že to oceníš v případě, že budeš muset přidat vesmírnou raketu, kdy prostě přidáš jen jednu třídu, vyplníš a jsi zobliga. Navíc jako bonus dostaneš možnost vytvořit například další auto, které ale nejede na diesel, ale používá jiné palivo.

To by snad bylo vše. Pokud něco nechápeš, tak mi napiš.

Tak užívej a já jdu točit zeměkoulí.