Reverse Engineering требований. Часть 4. Исходный код как источник информации

Привет.

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

У многих коллег возникает иллюзия того, что хранить описание “истины” достаточно в кодовой базе. Как будто таким образом мы сокращаем расходы на поддержание документации и уменьшаем до нуля ошибки в ней, ведь правильная версия всегда одна и в репозитории. Действительно, сильной стороной восстановления информации из исходного кода системы является ее абсолютная достоверность в плане того, как система что-либо делает. Поэтому этот источник идеально подойдет для следующих ситуаций:

  1. Никто не знает как работает конкретная функция системы, исходный код — единственный источник такой информации.
  2. Требуется наивысшая достоверность информации (при этом стоит помнить, что ошибки могут вноситься в процессе чтения и интерпретации читающим)
  3. Нужно восстановить сложные формулы вычислений. Особенно если есть вероятность расхождения их с общепринятыми, или у нас есть множество их вариаций для крайних случаев.

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

Первое, с чем придется столкнуться — это синтаксис языков программирования,  архитектура фреймворков и корпоративные соглашения по написанию кода. По сути, нужно понимать весь технологический стек приложения и особенности обращения с ним в конкретной команде. Технологический стек часто включает в себя разные языки и их диалекты. Вдобавок некоторые системы написаны с использованием низкоуровневых языков программирования, работа с которыми требует еще более высокой квалификации. Сложности может добавить и то, что части кодовой базы могут быть намеренно обфусцированы, реверс инжиниринг которых не всегда возможен и легален.

Следующая сложность состоит в знании того, как устроено приложение. Если перед нами что-то сложнее hello world-а, это значит, что внутренняя структура достаточно сложна, и состоит из многих элементов. Большие системы часто имеют высокий уровень декомпозиции, т.е. вместо цельной картины имеют множество мелких слабосвязанных деталей. Чтение такого кода проблема даже для разработчиков (https://podcast.python.ru/all/readcode ), для решения которой у них есть свои инструменты, начиная от встроенных в IDE, заканчивая собственными наработками (например вот https://habr.com/ru/post/440786/ ).

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

Другая полярность, усложняющая попытку извлечь информацию не меньше хорошей декомпозиции — говнокод. Очень трудно разобраться как работает контроллер на восемь тысяч строк кода сдобренного комментариями “не трогать”, зависящий от десятков глобальных переменных, чье состояние может меняться объектами вне текущего фрагмента.

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

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

Редко вышеперечисленные сложности встречаются поодиночке, обычно мы имеем дело с их комбинацией: текущим состоянием кодовой базы, которое также включает в себя две вещи: технический долг и изменения. Изменения копятся естественным образом по мере поступления новых требований. У технического долга есть много разных причин и проявлений, их обсуждение за рамками текущей статьи. Так что я хочу упомянуть только несколько из них: обновление версий используемых технологий, устаревание библиотек, смена парадигм разработки и архитектуры системы. Все вместе они формируют лоскутное одеяло нового и старого подхода к написанию кода. Если добавить сюда еще изменяющиеся соглашения по стилевому оформлению, то получится смесь, которую прочесть неподготовленному человеку невозможно.

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

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

Однажды стояла задача восстановить описание системы флагов (пиктограммы, выводящиеся в таблице данных по отдельным бизнес правилам). Разработчики показали, где в кодовой базе расположен код, отвечающий за их вывод и цепочку классов / функций по которым проходит информация. Архитектурно это место было сделано из двух частей. Бекенд передавал набор бинарных отметок, который фронтенд укладывал в свою модель и через специальную функцию подгружал нужные пиктограммы, уже используя свою модель. Так как мне нужно было в первую очередь отображение, я начал раскручивать цепочки с фронтенда, а не от базы.

Разбирая один из флагов, я нашел странность.. Код запрашивал данные с API в виде объекта. В поле объекта должно было вернуться значение. Но в формировании ответа на стороне бекенда его не было. Походив вокруг да около, я пошел к разработчику с вопросом “а как, собственно, это работает?”. Разработчик потратил примерно с полчаса еще чтобы разобраться и ответить “а оно не работает”.

В какой-то момент (причину мы так и не нашли) код бекенда, отвечающий за отсылку и формирование признака, был удален. Но аналогичное удаление не было сделано для фронтенда. Там осталась модель с атрибутом, который по умолчанию получал undefined значение, предполагая, что при вызове он будет перезаписан. Но поскольку в ответе ничего не приходило, это значение оставалось нетронутым. Дальше, в месте формирования вывода, стоял тернарный оператор, который в случае значения True должен был подгрузить пиктограмму, но с точки зрения JS, undefined — тоже значение, которое приводится False. Поэтому оператор штатно выполнял приведение и направлял выполнение по альтернативной ветке — т.е. ничего не делал.

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

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

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

Добавить комментарий