Плохой код #4
Что может быть хуже непонятных имен и комментариев? Непонятные операции и действия!
В этой статье примеров всего три, однако, на мой взгляд, они довольно сложные. Подумайте над каждым из них, поймите, что делает код и что хотел им выразить автор. Есть ли ошибки в логике работы программы? Если да, то укажите, какие, и опишите способ их исправления.
Решение
Первый пример
Выражение if(time)
проверяет, не равно ли time undefined, null, false, 0, пустой строке или любому другому “false-сравнимову” значению. В данном случае скорее всего имелась ввиду проверка на 0, поскольку дальше на это самое time происходит деление.
Однако что будет, если time будет равно 1? Правильно, то же самое, что и если бы time равнялось 0 - деление на 0, поскольку выражение time*(time-1)
равно 0 в обоих этих двух случаях. JavaScript при делении на 0 не бросает исключение DivisionByZero
или подобное ему, а лишь возвращает бесконечность. Однако вряд ли автор имел ввиду именно это, и скорее всего quality
должно быть равно вполне конкретному числу, а не бесконечности.
Попробуем исправить этот код.
Теперь деления на 0 не произойдет. Однако, что будет, если в знаменатель добавится еще один множитель?
Нам пришлось найти еще один корень знаменателя, который равен -4, и проверять на него. Выражения в знаменателе могут становится все сложнее, и в конечном итоге мы можем найти корень неправильно, или запутаться в уже существующих корнях. Однако, если немного поразмыслить, мы поймем, что все, что мы делаем в if(...)
это проверяем, не равен ли знаменатель нулю. Давайте попробуем выразить именно эту идею “знаменатель не равен нулю” в самом коде.
Намного лучше. Теперь при добавлении новых множителей или слагаемых в finalTime
нам не приходится менять условия проверки на 0, ведь все, что нам нужно - это чтобы сам finalTime
не равнялся нулю. Выразив эти слова в самом коде, мы избавили его от возможных ошибок и улучшили его читабельность.
Второй пример
Подумаем о намерениях автора данного кода. Количество оставшихся людей равно общему количеству минус количество удаленных людей. Выглядит логично, и код вполне соответствует логике автора. Или нет?
Как вы думаете, в каком порядке будут происходить вычисления? Давайте попробуем расставить скобки, сделав все более очевидным.
Некоторые скобки лишние, и их вполне можно убрать.
Вроде бы все здорово. Однако, запустив код, мы заметим, что наша программа начала вести себя по-другому. Более того, она начала вести себя правильно! Раньше мы не замечали неправильного поведения программы из-за того что у нас отсутсвовали тесты (о них мы поговорим в главе 5).
В чем же дело? Ведь мы лишь добавили скобки, чтобы подчеркнуть наши намерения. Оказывается, дело в том, что в исходном коде у операций был не совсем тот приоритет операций, который мы ожидали. Если в нем расставить скобки так, чтобы не нарушить этот приоритет, получится совсем не то, что мы хотели.
Оказывается, у оператора ?:
приоритет меньше, чем у операции вычитания, поэтому и вычисляться он будет после него.
Стоит быть крайне аккуратным с различными вычислительным операциями, особенно, если вы точно не знаете их приоритет. Лучше поставить лишние скобки, которые подчеркнут ваши намерения, чем иметь трудноуловимые баги в вашей программе.
Стоит заметить, что если вынести итоговое количество удаленных людей в отдельную переменную, все проблемы исчезнут.
Или, если в дальнейшем нам не нужно старое значение переменной deleted
.
Третий пример
Проблема этого примера в том, что вообще не понятно, что в нем происходит. Если для программистов на С побитовые операции являются родными (хотя это и не избавляет от регулярных ошибок в них), то для программистов на JavaScript такие выражения могут быть затруднительны для понимания, поскольку JavaScript - язык более высокого уровня, и возиться с битами в нем приходится не часто.
По аналогии с прошлым примером, для большего понимания давайте сначала расставим скобки, чтобы показать приоритет операций.
Теперь поймем, что же это за операции вообще такие.
~
означает побитовое отрицание. ~
как бы переворачивает (на английском “flip”) значение каждого бита в числе - 0 превращается в 1, 1 превращает в 0. Например, если число a
в битовом представлении выглядит как 00010111
, то ~a
будет равняться 11101000
.
&
работает уже с двумя числами. &
это побитовое умножение, или логическое И. Оно равно 1 тогда и только тогда, когда оба бита тоже равны 1, иначе оно равно 0. Для большего понимания давайте построим таблицу истинности.
Если в числах a
и b
битов много, то эта операция применяется последовательно для каждой пары битов, стоящих на одинаковых местах. Например, если a
равняется 11001100
, а b
10110111
, то получается
Как видно, 1 получилось только там, где биты в обоих числах равны 1.
Теперь вернемся к нашему изначальному примеру. Чему равно ~1
? Если 1
равняется ...00001
(здесь количество 0 зависит от того, сколько бит JavaScript использует для хранения чисел), то ~1
будет равно ...11110
, то есть будет идти много единиц, а в конце будет 0.
Что же будет, если произвести побитовое умножение с этим числом? Из таблицы истинности, приведенной ранее, видно, что при битовом умножении на 1 всегда получается исходное число. 1&1 = 1
, 0&1 = 0
. При умножении же на 0 всегда получится 0. 1&0 = 0
, 0&0 = 0
. Получается, что исходное число останется нетронутым, кроме его последнего его бита.
За что отвечает этот последний бит? Если он равен 0, у нас получается какое-то четное число c
. Если он равен 1, у нас получается нечетное число c+1
.
Соответственно, если наше число из примера n
четное, оно остается без изменений, иначе, если оно нечетное, из него вычитается 1. По другому пример можно переписать так.
Такая запись немного длиннее, зато для ее понимания не нужно знать всю вышеописанную теорию о битах (хотя знать ее крайне полезно).
Еще лучше было бы определить для такой операции специальную функцию, и в ней творить любые оптимизации, если простой вариант нас не устраивает.
Таким образом мы спрячем оптимизированный и непонятный код в функцию, и пользователю не придется каждый раз думать, что же это за &~ 1
такое.
Пример выше не надуман, а взят из исходного Сишноого кода NodeJS. Как я уже говорил, программисты на С любят такие штуки, и в их среде такие операции более очевидны. Однако “простым смертным” разработчикам на JS следует быть осторожными с такими заумностями. Как говорится, “When writing code, don’t be stupid, but don’t be clever either” - “Когда пишешь код, не глупи, но и не умничай”.
Итог
Старайтесь писать ваш код так, чтобы он как можно понятнее передавал ваши намерения. Не бойтесь и не ленитесь поставить лишнюю скобку, объявить лишнюю переменную или функцию - ведь код всегда читается намного чаще, чем он пишется. Это очень важное правильно, о котором многие, к сожалению, забывают.
Спасибо!
Комментарии