Недавно я писал, что мне достался исходный код одной из моих любимых игр. Я планировал сделать ремейк игры и в целом закончил переделывать код, осталось порешать некоторые мелкие задачи, протестировать прохождение всех уровней (увы, изначально исходник и сами карты были с багами, поэтому надо всё заново проверить), отрисовать новую графику и сделать современный интерфейс.
В процессе я понял, что занимаюсь разбором чужого кода. А существует такая догма, мол, надо писать понятный код, потому что кто-то потом будет в нём разбираться. И вот, оказывается, я сам и разбираюсь.
Какие же существуют правила понятного кода?
Я упомяну всего лишь два.
1. Короткие функции
Где-то встречал наставления от какого-то гуру, что функция должна быть не длиннее 5 строк или вроде того. Если длиннее, надо разбивать её на другие функции. Иначе, мол, становится трудно читать.
Что ж, давайте посмотрим, что там у нас на реальном примере. А у нас там, например, функция, которая обсчитывает движение воды. И она занимает...
140 строк.
Хм, может её разбить на функции по 5 строк, как советуют гуру? Тогда у нас получится...
28 функций.
Интересно, как их вообще назвать? :)
Или вот функция, которая обновляет состояние ячейки игровой карты. Она занимает...
300 строк.
Сколько-сколько функций у нас получится, если её разбить?
60 функций.
Серьёзно?
А главное – трудно ли мне, вот лично мне, кто занимается разбором чужого кода, который я никогда не видел, трудно ли мне его читать и в нём разбираться? Нет, не трудно. Вот совсем, абсолютно не трудно. Читается как интересная книга.
Правда, я всё-таки вынес 59 (не 5, а 59) строк в отдельную функцию для удобства. Но это был рефакторинг.
Короче говоря, разбираться в длинных функциях было легко.
Пойдём дальше.
2. Понятный код
Что такое понятный код?
Это когда совершаемые действия понятны и просты. Не делай так:
a ^= a;
потому что это непонятно. А делай так:
a = 0;
потому что это понятно. Не делай так:
y += (x == 10);
потому что это непонятно. А делай так:
if (x == 10) y++;
потому что это понятно.
Это условные примеры, чтобы ухватить мысль.
Так вот, автор использовал одно интересное решение. Ячейки водяного пространства в симуляторе воды кодируются как объекты, у которых есть два параметра. Первый параметр это маска (есть вода, можно воде сюда течь, есть слив и т.д.). Каждый бит маски означает наличие какого-то свойства, и само по себе это решение вполне "народное".
Но также есть второй параметр – счётчик испарения. Он увеличивается с каждым игровым тиком, и когда достигает установленного значения, капля воды должна испариться.
Какое же решение придумал автор? Он хранит и маску, и счётчик в одном байте. Младшие 4 бита это маска, а старше их хранится счётчик.
Что нам говорят правила хорошего кода? Не морочиться, а хранить маску и счётчик отдельно, иначе будет непонятно. Кстати, я так и сделал у себя. Но вернёмся к решению автора.
Неожиданное открытие
Счётчик, размещённый в старших битах, надо увеличивать на 1. И тут я бы сходу (именно сходу, не думая) выбрал такое решение:
- Получить значение счётчика из старших бит. Для этого надо байт с маской и счётчиком сдвинуть вправо 4 раза.
- Увеличить счётчик на 1.
- Записать счётчик и маску обратно в байт. Для этого надо запомнить, какая была маска перед сдвигом, сдвинуть счётчик влево 4 раза, и добавить маску.
Итого, как-то так:
mask = byte & 15;
count = byte >> 4;
count++;
byte = (count << 4) | mask;
Возможно, потом я бы пришёл к тому же, что сделал автор, но так как я уже видел его решение, то предполагать поздно.
Автор заменил всю эту математику всего одним действием:
byte += 16
Вместо того, чтобы сдвигать позицию счётчика к "нормальному" положению, он сдвигает прибавляемую единицу туда, где находится счётчик. При этом хранимая маска от такого прибавления не меняется.
Единственное, что нужно скорректировать, это максимальное значение счётчика. Если в обычном варианте варианте оно равно 6, то в "сдвинутом" это будет 6*16.
Когда к счётчику прибавляется 1 шесть раз, он достигает значения 6, а когда прибавляется 16 шесть раз, он достигает значения 96, но и в том и в другом случае это будет шесть раз, что собственно нам и нужно.
Для тех, кому непривычна двоичная арифметика, могу пояснить на следующем примере:
Допустим, есть число 17, которое обозначает некие данные. Если к нему прибавлять 100, мы будем получать такие числа: 117, 217, 317 и т.д. Как видим, первая цифра 1, 2, 3.. является счётчиком, а хвост с данными 17 при этом никак не меняется. Но чтобы увеличить счётчик на 1, надо прибавлять не 1, а 100.
Так вот, с точки зрения гуру это очевидно было не простое и не "понятное" решение. Но действительно ли оно было непонятным? Нет, не было. Мало того, что я его понял, я ещё и порадовался его оригинальности.
Нужели дело в моей супер-понятливости? Нет, мои способности весьма средние.
Тогда в чём дело? А в том, что автор не забыл прокомментировать нужные моменты.
Выводы:
- Живой, реальный код всегда будет отличаться от примеров гуру
- Длинная функция это не обязательно нечитаемая функция
- Нестандартные решения имеют право на жизнь
- Комментарии это хорошо