Археологія програмного забезпечення: практичний метод для аналізу коду з минулих версій
Дізнайтеся про поетапний підхід до безпечного дослідження недокументованих кодових баз спадщини, від аналізу історії змін до рефакторингу без порушення функціонування системи.
Існує певний вид жаху, який кожен розробник зрештою переживає.
Ви відкриваєте файл — у ньому понад 4 000 рядків. Немає жодних коментарів, половина змінних мають назви на кшталт x2 чи tempFinal_REAL, а десь посередині знаходиться функція під назвою doStuff(), яка, судячи з усього, відповідає за облік оплат у всій компанії.
Ви запускаєте git blame, і слід веде до людини, яка покинула компанію шість років тому. Пошук у Slack не дає жодних відповідей щодо причин існування всього цього. Єдина людина, яка може ще пам’ятати, зараз у відпустці, і, чесно кажучи, вона, ймовірно, також не пам’ятатиме деталей.
Це — археологія програмного забезпечення.
Це не відображається ні на жодній схемі організації, ні у оголошеннях про роботу, але якщо ви більше року чи двох присвятили написанню програмного забезпечення, ви вже практикуєте це. Ви аналізуєте старий код так, як археолог на місці розкопок — повільно, уважно, намагаючись з’ясувати, про що насправді думали ті, хто його створив, навіть якщо вони давно зникли та не можуть нічого пояснити.
Нижче наведено посібник з ефективного виконання цієї роботи — без втрати здорового глузду чи порушення роботи систем.
Що насправді таке археологія програмного забезпечення?
Археологія програмного забезпечення — це дослідження, інтерпретація та з’ясування сенсу старого чи недокументованого коду, зазвичай для того, щоб можна було безпечно його підтримувати, розширювати чи зрештою замінити.
Це інша робота, ніж звичайне виправлення помилок. Виправлення помилок ґрунтується на припущенні, що ви розумієте систему та щось у ній зламалося. Археологія програмного забезпечення починається з протилежного припущення — ви ще зовсім не розумієте систему, і справжнім першим завданням є досягнення цього розуміння перед тим, як наважитися щось змінити.
Це різниця між механіком, який обслуговує автомобіль моделі, з якою працював роками, та тим, хто відновлює Peugeot 1962 року випуску, який він ніколи раніше не розбирав. Одна й та сама наборчик інструментів, але зовсім різний стан душі.
Більшість інженерів не обирають цю роботу — вони потрапляють у неї. Ви починаєте нову роботу, а через шість тижнів хтось передає вам заявку, пов’язану „зі старим обслуговуванням запасів“. Раптово ви більше не пишете новий код — ви займаєтесь розкопками.
Чому ця навичка важливіша, ніж визнають люди
Ось неприємний факт: більшість програмного забезпечення, яке насправді керує світом, не є новим. Це старе програмне забезпечення, поступово допрацьовуване протягом років, яке частково лише зрозуміле, і що тихо стає джерелом тривоги для того, хто формально за ним відповідає.
Банки досі використовують COBOL, написаний у 1970-х роках. Авіакомпанії організовують маршрути польотів за допомогою систем, які існують ще з часів до появи пілотів, які керують цими літаками. Навіть стартапи, що розвиваються з великою швидкістю, протягом року-двох накопичують застарілий код — код, створений під тиском термінів хтось, хто вже змінив команду, для вирішення проблеми, яка ніде не була описана письмово.
Якщо писати новий код — це єдине, що ви вмієте робити, ви обмежені лише новими проектами. Але якщо ви справді можете читати старий код — так само, як детектив оглядає місце злочину, — ви стаєте тим інженером, до кого звертаються команди, коли потрібно виправити щось складне. Це професійна перевага, яку рідко обговорюють відкрито.
Мислення археолога
Перш ніж перейти до конкретних технік, необхідно змінити підхід, що полегшить усе подальше виконання завдань.
Вважайте, що код колись мав сенс для когось.
Цей один підхід до переосмислення має більше значення, ніж будь-яка інша техніка з цього списку. Дивлячись на заплутаний, незрозумілий код, легко дійти висновку, що той, хто його написав, не знав, що робить. Але це майже ніколи не є справжньою причиною. Набагато частіше існував терміновий крайній термін, обмеження, яке вже не видно для вас, вимога системи, яка згодом зникла, або прохання, яке у 2016 році здавалося цілком обґрунтованим, але після цього більше ніколи не розглядалося.
Уявіть собі, що ви заходите до старого будинку та бачите несучу балку в дивному, здавалося б, випадковому місці. Здається, що вона не має жодної функції — поки ви не дізнаєтесь, що там раніше стояла стіна, яку знесли, і ця балка залишилася єдиним елементом, який стримує обвал даху. Кодові бази з історичним кодом сповнені саме таких балок. Перш ніж щось торкатися, ваше завдання — з’ясувати, що саме кожна з них тихо підтримує.
Застосування такого способу мислення приносить дві переваги: воно допомагає залишатися скромним щодо власних припущень та замінює розчарування на цікавість. Виявляється, що цікавість — набагато ефективніший інструмент для виявлення помилок, ніж роздратування.
Техніки для аналізу коду з минулих версій
1. Читайте історію змін, як щоденник
Історія змін у Git — це майже те саме, що справжня машина часу. Не обмежуйтеся переглядом поточного стану файлу — простежіть, як він до нього дійшов.
Виконання команди git log --follow щодо конкретного файлу часто розкриває справжню історію його розвитку: функція, яка була додана в поспіх перед важливою демонстрацією для клієнта, поспішний виправлення, опубліковане пізно у п’ятницю ввечері, або коментар на кшталт «тимчасовий рішення, видалити після запуску у третьому кварталі», який вже чотири роки застарілий.
Я натрапив на дивну конструкцію if, яка стосувалася лише одного ідентифікатора клієнта, без жодних пояснень. Перевірка записів git blame виявила примітку автора, у якій пояснювалося, що в продуктивних даних певного клієнта була опечатка, і ця перевірка мала слугувати тимчасовим рішенням, поки клієнт не виправить свою частину. Це тимчасове рішення залишалося в силі протягом п’яти років. Розуміння історії змінило спосіб, яким команда зрештою його видала — вони обережно підійшли до справи, здійснивши належну міграцію даних, замість того щоб просто видалити перевірку та сподіватися, що нічого не зламається.
2. Слідкуйте за даними, а не лише за кодом
Код показує, що *могло* статися. Дані показують, що *насправді* сталося.
Перейдіть та отримайте дані безпосередньо з бази даних. Подивіться на справжні рядки. Якщо у таблиці є стовпець status із значеннями на кшталт 1, 2, 7 та 99, не намагайтеся з’ясувати їхнє значення лише шляхом аналізу коду — отримайте фактичні записи для кожного значення та простежте, що з ними відбулося на практиці.
Розгляньмо поле type у старій таблиці замовлень, де в кодовій базі були враховані лише значення від 1 до 5, проте у продакшн-даних були тисячі рядків із значенням type = 0. Виявилося, що 0 означає „створено до того, як взагалі з’явилося поле type“ — це частина історії системи, яка зникла з поточного коду, але все ще залишалася видимою у даних.
3. Поговоріть із „привидами“ (тобто з людьми, які все ще працюють)
Навіть коли той, хто створював певну частину системи, давно зник, хтось поруч зазвичай має уривок контексту — той, хто спочатку найняв цю людину, інженер підтримки, який роками обробляв пов’язані заявки, або менеджер продукту, який досі пам’ятає „цей інцидент“.
Замість загальних запитань ставте конкретні, менш тискові. „Що робить цей код?“ спонукає до здогадок, оскільки є занадто нечітким. „Чи пам’ятаєте ви щось незвичайне щодо обробки повернень коштів близько 2021 року?“ достатньо конкретне, щоб пробудити справжню пам’ять.
4. Створіть карту перед тим, як щось чіпати
Археологи ніколи не починають розкопки випадково — спочатку вони складають карту місця. Застосуйте таку саму дисципліну до кодової бази.
Намалюйте, навіть у приблизному вигляді на папері чи в документі, справжній шлях обробки даних під час їхнього руху через систему: які елементи запускають інші, які частини зберігають інформацію у пам’яті, та які компоненти залежать один від одного. Вам не потрібна досконала схема — достатньо простого опису, щоб уникнути несподіваних проблем.
Один корисний трюк: оберіть конкретну реальну дію, наприклад „користувач скасовує свою підписку“, та простежте її від початку до кінця, записуючи кожен файл та функцію, через які вона проходить. Простеження цього одного ланцюга дій часто допомагає зрозуміти основні аспекти роботи всієї системи.
5. Складіть тести перед рефакторингом
Якщо у кодовій базі зовсім немає тестів, що є поширеним явищем у старих системах, стримуйте бажання виправити все за один раз. Натомість напишіть так звані тести характеристик — тести, які просто фіксують те, що код *наразі* робить, незалежно від того, чи є ця поведінка правильною.
Це дає вам дві речі одночасно: захист у разі подальших змін та необхідну ясність, адже ви не зможете точно описати поточну поведінку, якщо насправді її не розумієте. Більша частина користі полягає у самому процесі написання тестів, а не лише у їх виконанні.
6. Змінюйте по одній речі за раз
Як тільки ви нарешті зрозумієте складну систему, виникає спокуса переписати все заново у одному масштабному, „триумфальному“ запиті на зміни. Не робіть цього.
Системи наступних версій часто виявляються більш крихкими, ніж здається, саме тому, що жодна окрема особа не має повної картини всіх залежностей. Внесення невеликих, скасовуваних змін — по одній за раз, кожна з яких перевіряється перед подальшими діями — є способом уникнути створення чергового заплутаного коду, який доведеться розгадувати майбутньому археологові.
7. Документуйте те, що знаходите — для наступної людини
Усе, що вам вдається знайти та зрозуміти, варто зберегти. Запишіть це десь. Навіть короткий, неформальний, неповний документ під назвою на кшталт „Як насправді працює сервіс билінгу“ є подарунком для того, хто успадкує цей код після вас — а це може бути саме ви через шість місяців, коли забудете все.
Коротка історія: Функція, яку ніхто не зрозумів
У одній компанії певна функція отримала прізвисько „чудовисько“ — 900 рядків коду, прихованих глибоко в процесі оформлення замовлення, до яких ніхто не хотів торкатися. Нових співробітників попереджали про неї під час адаптації, наполовину як жарт, наполовину як справжню історію про наслідки недбалості, схожу на місцевий фольклор.
Зрештою хтось вирішив ретельно її дослідити замість того, щоб уникати. Замість спроби переписати код вони простежували реальні транзакції під час їх обробки цією функцією, визначали кожну гілку логіки та писали тести для перевірки кожного сценарію. Увесь процес зайняв близько тижня.
Те, що вони виявили, — це не хаос; це виявився досить послідовний системний підхід до роботи з п’ятьма різними постачальниками оплат, кожен з яких мав свої особливості та унікальні ситуації. Його створив хтось, хто вирішував реальні, конкретні проблеми в реальних умовах обмежень, без можливості потім все впорядкувати. Як тільки його повністю проаналізували, ця система вже не здавалась страшною. Вона залишалася складною, але тепер це була складність, яку хтось насправді розумів.
По суті, це і є вся суть цієї дисципліни: перетворення страху на карту.
Заключні думки
У археології програмного забезпечення немає нічого гламурного. Ніхто не вказує „вміння розшифровувати заплутаний код інших“ як ключову навичку у своєму резюме. Проте це залишається однією з найкорисніших, але водночас найбільш занедбаних навичок у інженерії програмного забезпечення.
Старий код не варто відкидати як сміття — це запис рішень, прийнятих під тиском та обмеженнями, про які ви можливо ніколи не дізнаєтесь повністю. Підходьте до нього з цікавістю, а не з осудом, і ви зробите більш безпечні зміни, ймовірно, вважаючи цей процес набагато більш корисним, ніж очікували.
Тож наступного разу, коли якийсь файл змусить вас захотіти закрити кришку ноутбука та вийти на вулицю, зупиніться та переведіть подих. Те, що ви бачите, — це не руїни. Це місце розкопок, яке чекає на розуміння.
Візьміть пензель, а не бульдозер.
Пов’язана література
- Дев’ять звичок на рівні коду, які роблять роботу старших інженерів більш надійною — розглядається дев’ять конкретних практик програмування — від захисних клоз у до суворого моделювання даних — які роблять код більш стійким, зрозумілим та легшим для виправлення помилок під тиском.
- Десять постійних звичок у JavaScript, які тихо підривають ваш кодовий базис — пояснюються десять поширених проблем у JavaScript та TypeScript, від слабкої еквівалентності до зміни стану, та показуються безпечніші патерни для їх заміни.