Найти тему
ZDG

Разбор чужого кода – какие выводы можно сделать?

Недавно я писал, что мне достался исходный код одной из моих любимых игр. Я планировал сделать ремейк игры и в целом закончил переделывать код, осталось порешать некоторые мелкие задачи, протестировать прохождение всех уровней (увы, изначально исходник и сами карты были с багами, поэтому надо всё заново проверить), отрисовать новую графику и сделать современный интерфейс.

В процессе я понял, что занимаюсь разбором чужого кода. А существует такая догма, мол, надо писать понятный код, потому что кто-то потом будет в нём разбираться. И вот, оказывается, я сам и разбираюсь.

Какие же существуют правила понятного кода?

Я упомяну всего лишь два.

1. Короткие функции

Где-то встречал наставления от какого-то гуру, что функция должна быть не длиннее 5 строк или вроде того. Если длиннее, надо разбивать её на другие функции. Иначе, мол, становится трудно читать.

Что ж, давайте посмотрим, что там у нас на реальном примере. А у нас там, например, функция, которая обсчитывает движение воды. И она занимает...

140 строк.

Хм, может её разбить на функции по 5 строк, как советуют гуру? Тогда у нас получится...

28 функций.

Интересно, как их вообще назвать? :)

Или вот функция, которая обновляет состояние ячейки игровой карты. Она занимает...

300 строк.

Сколько-сколько функций у нас получится, если её разбить?

60 функций.

Серьёзно?

А главное – трудно ли мне, вот лично мне, кто занимается разбором чужого кода, который я никогда не видел, трудно ли мне его читать и в нём разбираться? Нет, не трудно. Вот совсем, абсолютно не трудно. Читается как интересная книга.

Правда, я всё-таки вынес 59 (не 5, а 59) строк в отдельную функцию для удобства. Но это был рефакторинг.

Короче говоря, разбираться в длинных функциях было легко.

Пойдём дальше.

2. Понятный код

Что такое понятный код?

Это когда совершаемые действия понятны и просты. Не делай так:

a ^= a;

потому что это непонятно. А делай так:

a = 0;

потому что это понятно. Не делай так:

y += (x == 10);

потому что это непонятно. А делай так:

if (x == 10) y++;

потому что это понятно.

Это условные примеры, чтобы ухватить мысль.

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

Но также есть второй параметр – счётчик испарения. Он увеличивается с каждым игровым тиком, и когда достигает установленного значения, капля воды должна испариться.

Какое же решение придумал автор? Он хранит и маску, и счётчик в одном байте. Младшие 4 бита это маска, а старше их хранится счётчик.

Что нам говорят правила хорошего кода? Не морочиться, а хранить маску и счётчик отдельно, иначе будет непонятно. Кстати, я так и сделал у себя. Но вернёмся к решению автора.

Неожиданное открытие

Счётчик, размещённый в старших битах, надо увеличивать на 1. И тут я бы сходу (именно сходу, не думая) выбрал такое решение:

  1. Получить значение счётчика из старших бит. Для этого надо байт с маской и счётчиком сдвинуть вправо 4 раза.
  2. Увеличить счётчик на 1.
  3. Записать счётчик и маску обратно в байт. Для этого надо запомнить, какая была маска перед сдвигом, сдвинуть счётчик влево 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.

Так вот, с точки зрения гуру это очевидно было не простое и не "понятное" решение. Но действительно ли оно было непонятным? Нет, не было. Мало того, что я его понял, я ещё и порадовался его оригинальности.

Нужели дело в моей супер-понятливости? Нет, мои способности весьма средние.

Тогда в чём дело? А в том, что автор не забыл прокомментировать нужные моменты.

Выводы:

  1. Живой, реальный код всегда будет отличаться от примеров гуру
  2. Длинная функция это не обязательно нечитаемая функция
  3. Нестандартные решения имеют право на жизнь
  4. Комментарии это хорошо