Горе от Ума — почему IT-проекты пишутся долго и стоят дорого (иногда)

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

Готовится релиз. Сроки подходят. Мне скидывают странный баг: Наше приложение вдруг стало жаловаться на невозможность соединиться с соседним.

А почему не может? Защищённое соединение не устанавливается.

А почему не устанавливается? Файлы сертификатов для этого соединения не удаётся загрузить.

А почему файлы не грузятся? А потому что путь к файлам "отсутствует в конфигурации".

А если руками залезть и глазами посмотреть - присутствует. Чудеса! Эффект Шрёдингера!

О проекте

Приложение - часть большого проекта. В то же время и само приложение не одна большая "программулина", а состоит из нескольких отдельных "сервисов". В сумме это больше 4500 исходных файлов кода, из которых, правда, 2500 это файлы тестов. Из файлов с тестами, правда, примерно половина - пустые, потому что специальная умная утилита создаёт их по шаблону в любой новой директории с исходниками.

Сервисы приложения используют некую "базу данных", её я беру в кавычки т.к. это довольно специфическое и притом небольшое хранилище - но для нашего рассказа это неважно. Знатоки сразу подметят "а хорошо ли что все сервисы вместе ходят в базу?" Действительно, этого часто стараются избегать - но тут сложилось что, в частности, некоторые параметры конфигурации хранятся в этой самой "базе" - вот как раз чтобы все сервисы могли удобно сходить и их прочитать.

И, как видим, один из параметров (один ли?) вдруг не читается.

Расследование показало

Смотрю в код сервиса, который не смог прочесть злосчастный "путь к сертификатам", а там что-то в духе такого (язык и названия изменены):

В одном из файлов инициализирующих сервис вызывается функция для определения пути к сертификатам:

function initSecureConnection() {
    // ...
    certificatesPath = certificatesParametersReader.ReadCertificatesPath()
    if (certificatesPath == ERROR_NOT_FOUND) {
        // кидаем ошибку
    }
    // ...
}

Не хухры-мухры, у нас целый вспомогательный класс для чтения параметров связанных с сертификатами - и пути к ним в частности. Как-то так:

class CertificatesParametersReader {
    // ...

    function ReadCertificatesPath() {
        // тут строк 30
        // лезем в базу...
        // устанавливаем сессию...
        // проверяем разные ошибки...
    }

    // ...
}

В целом написано всё солидно. Этот класс для чтения параметров густо покрыт "юнит-тестами", в частности только на метод ReadCertificatesPath который нас интересует - тестов штук 7 написано - проверяют разные случаи проблем при обращении к базе и так далее.

В общем, прихожу к выводу что тут проблемы нет. Если нужный параметр в базу записан - то сервис его прочтёт.

Значит его нет, когда его читают.

Но я же его вижу...

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

Найти в проекте из тысяч файлов кто записывает строчку в базу - это всё же не иголку в стоге сена искать. По имени под которым данный параметр в базе находится, отыскиваю нужный файл. Тут всё не так развесисто, просто есть метод для записи:

function fillConfig() {
    // ...

    if (fillCertificatesPath(session) == ERROR) {
        return new Error("error while filling certificates path")
    }

    // ...
}

// ...

function fillCertificatesPath(session) {
    session.WriteData(certificatesPathParamName, certificatesPathParamValue)
}

Ну хорошо, включим уровень логгирования на котором видно, когда происходит запись в базу. Перезапустим сервис... Вот, опять ошибка... Смотрим, анализируем... Ну конечно:

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

Потому что проблема даже не в этом!!!

Стал я размышлять как тут лучше поступить - то есть, перебирать в голове упомянутые "много способов". Поскольку речь идёт о релизе, тут надо действовать минимальными и наиболее безобидными изменениями, чтобы не задеть чего-нибудь ещё, чтобы не пришлось много перетестировать.

Самым очевидным кажется подсунуть куда-то вокруг (или внутри) функции чтения повтор с задержкой, ну в духе:

function ReadCertificatesPath() {
    // вынесем тело в отдельную функцию ниже, а здесь будет цикл
    delay = 100 * millisecond
    startTime = currentTime()
    while (currentTime() - startTime < 5000 * millisecond) {
        path = readCertificatesPathAttempt()
        if (path != ERROR) {
            return path
        }
        sleep(delay)
        delay = delay * 3 / 2
    }
}

function readCertificatesPathAttempt() {
    // прежние 30 строк тут
}

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

Но прежде чем делать это...

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

    session.WriteData(certificatesPathParamName, certificatesPathParamValue)

Вот что это за certificatesPathParamValue - при первом чтении я не обратил внимания - а сейчас глянул на определение (оно, как модно, оказывается в другом файле):

const certificatesPathParamValue = "/etc/trusted/common.crts"

То есть это константа. Вся эта свистопляска с методами для записи значения в базу, с мудрёным классом для его вычитывания, с тестами на этот класс - ради передачи "захардкоженного" значения, которому не судьба в жизни никогда поменяться.

Я ещё подумал - может, чего-то не догоняю? Может этот параметр какими-то силами можно проставить извне, поменять путь к сертификатам (зачем? даже звучит странно - но мало ли...)

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

Анализ

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

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

Как же это получилось? Откуда такой сложный код для выдачи константы? Можно подумать что это результат исторический - быть может когда-то давно это не было константой... И только после в результате рефакторинга сложилось так, как сложилось.

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

Итак, как же это получилось?

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

Нельзя просто так взять и вычитать параметр.

Нужен класс Reader к параметру.

Нужен интерфейс на этот класс.

Нужны тесты на этот класс.

Нужны "моки" на этот интерфейс.

И только одна маленькая деталь упущена - параметр вообще не требовалось вычитывать. Но поскольку проект такой здоровый, поскольку запись в другом сервисе, поскольку над кодом работают человек 20 программистов - это прошло незамеченным.

Незамеченным? Да нет, ведь, как я сказал, это не случайность - подобных константных параметров в коде не так уж мало...

Это образ мысли.

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

И это не в какой-то "сомнительной шараге" - контора солидная, в топах разных рейтингов (и на хабре / хабр-карьере в том числе). Тысячи человек народу. Они пишут миллионы файлов исходного кода. Представьте процент этой "клюквы" в этих миллионах файлов. Пересчитайте это во время и деньги.

Хуже того - как видим, временами эта ересь ведёт к сбоям - хорошо что поймали до релиза, а не после. Было бы сложнее, больнее, дольше и дороже разбираться.

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

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

Не зря говорится: Don't be too clever.

@RodionGork
24.10.2025 10:40 UTC
Первоисточник

Комментарии

@ncix
24.10.2025 06:28 UTC
+7

KISS

@Sau
24.10.2025 06:29 UTC
+6

YAGNI

@RodionGork
24.10.2025 06:47 UTC
+4

Как ни странно, в требованиях вакансий на хедхантере я часто читаю что хотят знание принципов DRY и SOLID например - но по KISS почти не встречается. Можно оптимистично думать что "это подразумевается" или пессимистично что "никто не хочет Simple" :(

@ASenchenko
24.10.2025 06:40 UTC
+1

Тут где-то рядом статья была на тему оверинжениринга. Неплохая.

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

Но может быть и закладка буквально на следующий релиз, просто в этом не успели отладить изменение параметра.

Вам бы выяснить этот вопрос, вот соседи то потом удивятся.

Но и это ... Очередь записи/чтения у Вас пока что кривая осталась :)

@RodionGork
24.10.2025 06:46 UTC
+1

закладка буквально на следующий релиз

если бы это был какой-то более волатильный параметр чем путь к файлам сертификатов :)

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

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

Но и это ... Очередь записи/чтения у Вас пока что кривая осталась :)

Ох, не сыпьте соль на рану... Для релиза-то я быстро пропатчу, а над мастером предстоит много и нудно думать :) эта "очередность" это конечно бомба замедленного действия - а там рядом и другие параметры вычитываются, разбираюсь какой откуда берется...

24.10.2025 06:51 UTC
+1

Родион, Вам контекст разумеется виднее на месте.

Но у Вас релиз, а у меня утренний кофе. Пока адреналинчик по нулям - решил чисто из опыта напоминалочку повесить :)

24.10.2025 06:53 UTC
+3

хех, кто-то с утра кофий пьёт, а кто-то просыпается чтобы кормить небольшую банду котов - ну и лоточки убирать :) спасибо за отклик в любом случае!

24.10.2025 22:09 UTC
+1

О, спасибо что напомнили — я сегодня чот забыл лоток у своих почистить :)

31.10.2025 08:56 UTC
+1

ну да, вдруг в следующем релизе имя компании станет динамичным :)

А вот это вы зря иронизируете. Мой опыт работы в крупной страховой компании говорит, что нет ничего более стабильного чем внезапные изменения :)

Нас, лет за 20, раза 4 покупали все более громадные монстры и первый раз были муки с ребрендингом. Зато чем дальше - тем легче, с опытом-то.

P.S. Забавно, говоришь что работаешь на одном месте больше 20 лет - многие в шоке, а по факту - сменил 4 места на принципиально другое ибо в каждом покупателе свои тараканы... Только успевай название компании менять в параметрах :)

@
25.10.2025 03:37 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@Panzerschrek
25.10.2025 06:05 UTC
+1

Смотрите на это иначе - столько людей заняты, стабильно получают зарплату и увеличивают своей деятельностью ВВП!

25.10.2025 07:57 UTC
0

Кстати, насчет ВВП я не уверен. Если бы проект делался на иностранцев, то это в любом случае был бы приток финансирования в страну. А если это разработка на внутренний рынок, притом, не дошедшая пока до "продакшена", то можно рассматривать как вообще частное дело самой компании финансируемой из её же собственных свободных финансов (тут я подробностей не знаю). Скорее всего в учете ВВП это не учитывается. Хотя зарплату-то работники получают.

@RodionGork
25.10.2025 07:54 UTC
0

типа исправил. А потом еще статью написать про это!

наверное тут я не очень чётко пояснил поэтому вы не поняли - "минимальное" исправление сделано только в релизной ветке, которую уже нельзя сильно менять

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

А потом еще статью написать про это!

Статью-то подобную написать дело нехитрое и много времени не заняло - собственно я её как "заметку" охарактеризовал выше, просто в "пост" не лезет. Написана же она с чёткой прикладной целью - не только "привлечь внимание общественности" (не думаю что многие схватятся за голову и начнут писать код более прилично) - но и тыкать ею в нос собственным менеджерам. Терпеть не могу объяснят одно и то же по сто раз. И в этом смысле ваш возмущённый комментарий очень в тему, спасибо :)

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

25.10.2025 14:24 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@Atorian
25.10.2025 10:16 UTC
+2

Писать простой код могут не только лишь все.

Сочувствую. Но эта проблема в образе мышления. Кому-то изначально платили за строки кода. А потом он пришёл в продукт, а привычка осталась.

Сам сейчас таких переучиваю. Начну с выставления правильных kpi

@RodionGork
25.10.2025 10:23 UTC
0

Про выставление правильных KPI в случае успеха, по возможности, напишите потом - думаю, будет поучительно - может удастся и наших управленцев на основе Вашего опыта к чему-то подвигнуть :)

25.10.2025 11:00 UTC
+1

Не вопрос. Пингану потом.

Гипотеза проста - хороший код ускоряет разработку. Меньше кода - быстрее поставка.

Так что я могу попросить разрабов сообщать на ретро или тех синке о подобных проблемах. И попрошу чтобы они сразу исправляли это, а не создавали доп тикеты.

Придумаю какой-нибкдь бонус небольшой за исправление минимум 1 такого места.

И добавлю сонар- буду смотреть на общий maintainability index для начала. А когда интегрирую DORA - посмотрю корреляцию.

Я получил код в плачевном состоянии, тут про метрики и бест практисы и говорить нечего. Скорее сборник примеров как делать не надо. По этому мне еще предстоит интегрировать все эти инструменты и метрики, выстраивать культуру и пайплайны.

25.10.2025 11:35 UTC
0

Меньше кода - быстрее поставка.

хех, золотые слова же :)

@megadrugo2009
26.10.2025 08:21 UTC
+1

Смущает что в одну таблицу пишет несколько сервисов.

И такие вещи обычно решаются через переменные окружения. Которые подгружаются из безопасного хранилища, например vault.

@Anarchist
26.10.2025 22:30 UTC
+2

Сервис сам по себе более или менее понятен - тестирование. Мокнуть и протестировать что-то ещё, чтобы не настраивать окружение. Но зачем база, чтобы хранить константу?

@MANAB
29.10.2025 06:23 UTC
+1

А потом это все кому-то поддерживать лет 20, если проект таки взлетит. И страдать от того, что все так замудрено - поменял пару строк и все развалилось.

@Caraul
30.10.2025 16:11 UTC
-1

Вся эта свистопляска

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

@RodionGork
31.10.2025 10:33 UTC
0

не стоит давать оценки, просто исходя из увиденного.

а почему вы думаете что я "просто исходя из увиденного"? я уже год на проекте и знаю ребят кто это безобразие писал, знаю как они пишут, но не упоминаю сколько ещё странных косяков, иногда критичных, в их проекте поправлял :)

так что у меня достаточно оснований

31.10.2025 16:28 UTC
+1

безобразие писал

странных косяков

в их проекте поправлял

"Я д'Артаньян, а вы все гвардейцы кардинала" :) - я даже утвердился в свое мнении. Тем более что "уже год" в отрыве от масштаба проекта смысла не имеет. Да и совет был не столько лично Вам, сколько читающим эту статью - не стоит повторять анекдот "Этот дом построили пи@#$@сы".

12.11.2025 14:36 UTC
0

хорошо-хорошо, почему бы вам не написать статью о том что чем кода больше тем лучше, ведь никогда не знаешь что может случиться :)

11.11.2025 11:43 UTC
+1

после того, как я увидел в Java проекте, на который меня взяли разгребать косяки,

if(a==b) return true;

else return false;

ничему уже не удивляюсь.

@rombell
11.11.2025 11:42 UTC
0

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