Недавно столкнулся с необоснованным, на мой взгляд, злоупотреблением конструкцией switch в PHP (язык в этом вопросе — важный момент!). И, кажется, сторонников у меня по этому вопросу особо и нет, как нет и достаточно авторитетного источника, где бы было сказано, чем такое использование плохо. Авторитетным мой бложик не назовёшь, но пусть эта запись станет той песчинкой, что, возможно, поможет сформировать будущий фундамент, а также даст подкрепление альтернативного мнения. Всё написанное далее — моё личное мнение. Я могу ошибаться, поэтому буду рад замечаниям, уточнениям, дополнениям и аргументам. Кроме того, если вы приверженец switch (true), пожалуйста, напишите мне свои аргументы, почему может быть удобнее/выгоднее/эффективнее использовать эту конструкцию?

Чем плох switch (true)?

Первое, что приходит в голову — это семантика. Скажем так, классический пример использования switch предполагает перечисление уникальных случаев (case).

switch ($value) {
case 1:
// do something
break;
case 2:
// do something else
break;
case 3:
// do another thing
break;
default:
// default behaviour
}

Тут все кейсы разные и, по умолчанию, порядок следования кейсов значения не имеет. При этом есть прямая связь между тестируемым значением ($value) и кейсами.

Использование switch (true) ломает представление как об уникальности кейсов, так и о об отсутствии важности порядка следования кейсов.

То есть, в моем представлении, правильная конструкция switch с boolean значением должна выглядеть так:

switch ($value) {
    case true:
        // do something
        break;
    case false:
        // do something else
        break;
}

(Но тут сразу явно видно, что вместо неё надо использовать if/else.)

В этот момент мы подходим ко второму моменту, чем плоха данная конструкция. Появление приоритета кейсов делает конструкцию более подверженной ошибкам, так как явно этот порядок никак не выражается и не “навязывается”. И это даже без уже давно известной критики (и классической ошибки) break.

Ещё одной проблемой конструкции switch/case в PHP (не только switch (true)) я считаю лишнюю вложенность кода. В некоторых современных языках, где к проблемам этой конструкции подошли серьезнее (Go, Swift) не только заставили явно обозначить желание провалиться в следующий кейс (что, собственно, и является классической ошибкой с break), но и форматировании по умолчанию (go fmt, и форматирование в Xcode) убирают лишнюю вложенность кода.

Вложенность плоха тем, что обычно свидетельствует об увеличении цикломатической сложности кода. Здесь по сути такого усложнения нет, но мы будто намеренно хотим себе навредить и себя же обмануть, поставив обманчивый указатель (в виде лишнего отступа): “Здесь сложность. (Но это не точно.)”

И, наверное, последний момент, который я сейчас вспомнил. Приведу частный случай использования switch (true):

switch (true) {
case $a instanceof ClassA:
return 1; // обратите внимание, это версия лишена проблемы с break, как будто
case $a instanceof ClassB:
return 2;
case $a instanceof ClassC: // а вот это удобное на первый взгляд схлопывание условий на самом деле возвращает проблему с break :)
case $b instanceif ClassA:
return 3;
}

Тут налицо все вышеописанные проблемы. Отдельно хочется отметить отсутствие связи между кейсом и тестируемым значением. Я в этот пример также намеренно добавил кейс, где неожиданно появляется какое-то $b, о котором ранее никто не знал. Именно так нередко и бывает в реальном коде.

Собственно, чем плох этот пример — он намекает, что никакого switch тут быть не должно, его место должен был занять полиморфный вызов. Кстати, если бы мы заменили тут switch (true), например, на if/elseif/else, то лучше бы не стало по той же причине.

Когда использовать switch?

Как уже понятно, я против использования switch (true). Но это ещё не всё: мне кажется, что использование switch в PHP должно быть ограничено фабриками, которые помогают с реализацией полиморфизма.

Возможно. Возможно, есть другие оправданные случаи, которые скорее всего будут похожи на мой первый классический пример использования switch.

Что же использовать?

Вы не поверите, но специально для работы с логическим типом (boolean) у нас есть старый добрый if.

И он лишён описанных выше недостатков switch (true).

И да, его тоже можно использовать неправильно — аналог кода с instanceof легко найти и в варианте с if/elseif/else.

Вкусовщина?

Конечно! Меня очень забавляет, когда на критику использования чего угодно отвечают: “Вкусовщина!” Для меня как красная тряпка.

Действительно, не важно что мы выбрали: if или switch, чёрный хлеб или белый, нарезать дольками или кольцами. Это всегда будет дело вкуса и предпочтений. Но за ними всегда стоят аргументы: с чёрным хлебом я наберу меньше килограмм, дольки скорее поместятся ко мне в рот, а по поводу выбора между if и switch я уже написал выше.

Но дело не ограничивается этим: какие отступы выбрать (ага, вечный холивар пробелы против табуляции), какой алгоритм использовать, как назвать метод, какой длины должен быть класс, имеет ли право сущность иметь только геттеры и сеттеры?

Не будем обсуждать холиварные вопросы. Какой алгоритм использовать? Кто-то скажет, конечно, более эффективный по процессору. Другой ответит, что более эффективный по памяти. Третий предпочтёт более простой для понимания. И всё это будет делом вкуса, пока на первое место не выходят другие требования.

Если мы хотим снизить вероятность появления ошибки из-за человеческого фактора, то выберем более простой алгоритм. Если же перед нами встаёт вопрос ограничения ресурсов, мы будем решать эту проблему, оптимизируя работу программы.

Код пишется для людей

Стоит ли об этом напоминать, поэтому я обычно за более простой вариант. Что важно, код пишется (не только с нуля, речь и про внесение доработок) людьми. Поэтому я предпочитаю выбирать такой вариант, который помогает избегать возможности появления ошибки. Кто-то скажет, что на это у нас есть тесты, но и их пишут люди. Я сам хороший пример написания не лучших тестов, которые могут пропустить вполне очевидную ошибку. Что уж говорить про те ошибки, о которых не подумал автор теста.

В общем, использование некоторых техник защитного программирования как раз делает свой вклад в повышение качества кода.

И в данном случае даже выбор конструкции if вместо switch (true) я отношу именно к такому защитному механизму.

Оптимизация со switch (true)?

Выбор в пользу использования switch иногда аргументируют оптимизацией. Мол, не надо выполнять выражение несколько раз.

Это справедливо для такого варианта:

switch ($service->doSomeExternalRequestsAndMath()) {
case 1:
// ...
}

Но о какой оптимизации мы говорим в варианте с switch (true)? Я специально повторю свой пример с instanceof тут:

switch (true) {
case $a instanceof ClassA:
return 1; // обратите внимание, это версия лишена проблемы с break, как будто
case $a instanceof ClassB:
return 2;
case $a instanceof ClassC: // а вот это удобное на первый взгляд схлопывание условий на самом деле возвращает проблему с break :)
case $b instanceif ClassA:
return 3;
}

Читаемость

Вот тут ярче всего может проявиться противоборство вкусов (как с пробелами против табов). Несмотря на мой аргумент с лишним отступом, кто-то найдёт это улучшением —код со switch более “разряженный”, читать легче. Другой скажет, что в варианте с if код выглядит более сбитым, что позволяет вычленить из спагетти логический блок (а потом возможно выделить его и в отдельный метод?).

Рассмотрим два упрощенных примера.

Пример первый:

switch (true) {
case $a > 911:
$exchange->publish(['a' => $a]);
$db->saveNewValue($a);
break;
case $a > 112:
$logger->log('problem here!', ['a' => $a]);
break;
// ...
}

if ($a > 911) {
$exchange->publish(['a' => $a]);
$db->saveNewValue($a);
} elseif ($a > 112) {
$logger->log('problem here!', ['a' => $a]);
} // ...

Мы видим, что вариант с if такой же плохой как и вариант со switch, только менее многословный. Для меня это уже становится флажком, что понять проще.

Пример второй:

switch (true) {
case $a > 911:
$this->registerIncomingValue($a);
break;
case $a > 112:
$this->reportProblem($a);
break;
// ...
}

if ($a > 911) {
$this->registerIncomingValue($a);
} elseif ($a > 112) {
$this->reportProblem($a);
} // ...

Мы упростили тела наших ветвлений и код стал гораздо проще и понятнее в варианте с if, а в switch это улучшение как будто потерялось на фоне многословности.

Но if нередко винят в плохой читаемости из-за “тяжёлого” условия, которое как будто читается лучше на отдельной строке (менее загруженной лишними символами) case. В нашем примере нет такого условия, но всё же продемонстрирую, как легко это исправляется и попутно улучшает читаемость кода (тут имеется в виду: делая язык более натуральным):

if ($this->isNormal($a)) {
$this->registerIncomingValue($a);
} elseif ($this->isTooLow($a)) {
$this->reportProblem($a);
} // ...

Categorized in:

Tagged in: