Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Я собрал худшее из худшего! Оказалось, что хороших практик — море, и разбираться в них долго, а вот плохих, реально плохих, — считаные единицы.
Понятно, что плохие практики не отвечают на вопрос: «А как делать-то?» — но они помогают быстро разобраться в том, как не делать.
Мы часто спорим про архитектуру и хотим друг от друга знания разных правильных практик проектирования, лучшего мирового опыта и вот этого всего. Понятно, что в реальном мире это совсем-совсем не так. И худших практик часто достаточно, чтобы начать договариваться, как не надо.
Итак, мой любимый антишаблон — поток лавы. Начинается всё просто: в новый проект можно добавить код из старого проекта копипастой. Он там делал что-то полезное, пускай делает почти то же самое полезное в новом проекте. Вот только нужно закомментить один кусок, а в другом месте — чуть дописать. Примерно через три переноса без рефакторинга образуются большие закомментированные участки, функции, которые работают только с частью параметров, сложные обходы вроде «выльем воду из чайника, выключим газ, и это приведёт нас к уже известной задаче кипячения чайника» и так далее.
Это если команда одна. А если разработчики на пятом проекте новые, то начинается самое весёлое — этот сталактит надо ещё прочитать.
Очень часто я вижу лава-код в проектах аутсорсинговых компаний, потому что они используют свою кодовую базу по разным заказчикам как такой своеобразный иннерсорс. А «междисциплинарный» код как раз хорошо обрастает отключаемыми участками и переопределяемыми функциями.
Поток лавы
Итак, дамы и господа, позвольте представить: первый на нашей сцене — поток лавы! В целом про него я уже примерно рассказал, и вы представляете, откуда он такой берётся и что делает. Обратите внимание: паттерн подкупающе хорош в кратковременной перспективе и становится плох только на дальней дистанции. И смертельно плох на очень дальней. Поэтому на всякий случай давайте формально отмечу основное.
Симптомы:
- Непонятно откуда взявшиеся переменные.
- Не относящиеся к задаче фрагменты кода.
- Очень странное покрытие документацией, много выглядящего важным за её пределами.
- Архитектура слеплена из предыдущих трёх архитектур.
- «Да кто её будет смотреть, просто подключайте к проекту!»
- Устаревшие интерфейсы.
Что будет дальше: а дальше либо всё это разбирать, либо разработчики возьмут ветку целиком и унесут дальше, потому что её слишком сложно рефакторить, но она работает. Через три итерации код невозможно будет задокументировать во внятные сроки и в него нельзя будет быстро внести изменения.
В API-driven-подходе + Domain-Driven Development такого в принципе не должно случаться, а мы хотим думать, что придерживаемся этих идей. Точнее, архитекторы уверены, что придерживаемся, а дальше уже как пойдёт в конкретных командах.
Как и в других случаях ниже, тут возможно отдельное развлечение — прогулка по минному полю, когда вы не знаете, какой фрагмент кода как точно работает. И некоторые, казалось бы, очевидные и прямые решения приводят к интересным для отладки последствиям. Если к вам подходит более опытный разработчик и говорит: «Ты так не делай, сейчас расскажу, как обойти, эта функция вообще опасная, с ней лишний раз не связывайся» — это как раз оно.
Blob
Это жирнющая штуковина, которая делает всё. Обычно это класс, который разросся сначала до уровня универсального решателя всего-всего внутри проекта, а потом чуть ли не стал операционной системой. Появиться он может из-за неправильных решений в архитектуре или просто инкапсуляции всего подряд в страшной спешке. Либо из-за последовательного улучшения какого-то класса несколькими поколениями разработчиков, которым просто вбить костыль и пойти дальше.
Эта штука перестаёт быть в полной мере объектно-ориентированной и возвращает нас в мрачные времена расцвета программирования. Собственно, на практике я встречал такие конструкции только в банковском легаси. Всё, что было примерно после нулевых, уже проектировали с головой независимо от того, объектное или функциональное там программирование. Даже старые монолиты могут быть разделены на функции так, что получается модульная структура, легко переживающая изменения (она только релизится вместе, а разрабатывается независимо).
Опять же очень часто блоб выступает просто первым симптомом ещё каких-то проблем. Я видел смелых людей, пытавшихся такое рефакторить (да и сам участвовал пару раз). Так вот, за этим нагромождением обычно бывают сразу десятки частных случаев плохих практик.
Думаю, что несильно ошибусь, если скажу, что у каждого банка из топ-10 в России есть система, которой больше 15 лет и в которой есть штуковина размером с фреймворк для бизнес-логики, вокруг которой аккуратно расставлены обработчики и разные внешние интерфейсы. Сама же система не поддаётся членению и не может быть переписана в ближайшие годы.
Непрерывное устаревание
Здесь всё просто: нужно смотреть, что сейчас новое и правильное из технологий, и внедрять то, что было новым хотя бы пару лет назад. Ну или не внедрять, но осознанно, понимая, что дальше произойдёт с вашим кодом.
Частный случай устаревания — когда какой-то компонент перестаёт поддерживаться, и поддержку фактически оказывает команда разработки. Это даже веселее, чем просто работа на старой технологии.
Заканчивается это либо тем, что система умирает от старости и больше становится не нужна, либо рефакторингом, больше похожим на переписывание с нуля.
Функциональная декомпозиция
Это когда разделение на классы делается не по функциональному уровню, а по частям бизнес-процесса, что в итоге даёт очень дегенеративную архитектуру.
Это когда тащат бэк на фронт и наоборот: фронт запихивают куда-то внутрь классов бэка. Это когда запрос остатков внезапно открывает соединение с каким-то веб-сервисом, это когда в функции сложения двух чисел строится красивый отчёт для оператора и так далее.
Вот простой пример функциональной декомпозиции:
Выяснять плюсы и минусы ООП тут не планируем, это лишь простая демонстрация усложнения понимания в процедурном решении.
Процедурная версия расчёта кредита для клиента.
«Плохой» вариант:
Объектно-ориентированная версия расчёта кредита для клиента.
«Хороший» вариант:
Распутывание такого кода, когда он на всём проекте, — отличное приключение. Обычно такая практика случается, когда на проекте — несколько джунов, и нет никого, кто выступил бы архитектором. Или когда разработчики быстро клепают MVP, понимая, что завтра этот код полетит в корзину, а в итоге он используется годами. Иногда я вижу такое в коде аутсорс-компаний, которые берегут время разработчиков.
Итог в том, что система строится так, что провести рефакторинг практически нельзя. Приходится с этим жить и постепенно делать из этого блоб.
Лодочный якорь
Это когда вам досталось много кода, и весь этот код бесполезен в рамках задачи. Я такое видел после слияний-поглощений компаний, когда из двух систем компаний в итоге делают одну. Или когда вам достаётся код без базы данных, часть логики реализована в базе — и удачного вам сбора требований и попыток доработки.
В общем, так бывает, что у вас есть что-то не просто неиспользуемое, а ещё и мешающее. Такой код надо вырезать целиком как можно быстрее.
Золотой молоток
Эту практику я вижу практически постоянно. Когда у вас есть молоток, всё вокруг становится похожим на гвозди.
Когда у вас есть работающий код или технология, обвязанная большим количеством кода, кажется, что ими можно решить любую проблему.
Я встретился с золотым молотком в момент, когда Кафка стала хорошо протестированной шиной. А у нас в проекте везде применялась интеграция через IBM MQ. Пришлось потратить много времени на защиту нового решения интеграции и убедить коллег в её преимуществах для нашей очередной интеграционной задачи.
Смысл в том, что золотой молоток позволяет решать задачу без когнитивной нагрузки. Просто взяли и забили известной технологией. Новая же технология требует изучения, поиска подводных камней, потом непонятно сколько кода переписать, непонятно, что там с ИБ, непонятно, как поддерживать. Но всё это можно оценить. И если преимущества перехода выше, чем все эти затраты, то переходить имеет смысл.
Важно понимать: это обоснованное желание остаться в рамках старой технологии, или «синдром утёнка», когда первая изученная технология кажется лучшей.
Спагетти-код
Вообще-то это группы объектов, написанные для конкретного бизнес-процесса, но не предназначенные для использования в другом. На практике это скопипащенные куски кода, которые в плюс-минус одинаковом виде встречаются раз так 5–10 минимум в разных модулях.
Пример спагетти-кода из Linux SCSI driver (с небольшими изменениями) начала нулевых:
wait_nomsg:
if ((inb(tmport) & 0x04) != 0) {
goto wait_nomsg;
}
outb(1, 0x80);
udelay(100);
for (n = 0; n < 0x30000; n++) {
if ((inb(tmport) & 0x80) != 0) { /* bsy ? */
goto wait_io;
}
}
goto TCM_SYNC;
wait_io:
for (n = 0; n < 0x30000; n++) {
if ((inb(tmport) & 0x81) == 0x0081) {
goto wait_io1;
}
}
goto TCM_SYNC;
wait_io1:
inb(0x80);
val |= 0x8003; /* io,cd,db7 */
outw(val, tmport);
inb(0x80);
val &= 0x00bf; /* no sel */
outw(val, tmport);
outb(2, 0x80);
TCM_SYNC:
/* ... */
small_id:
m = 1;
m <<= k;
if ((m & assignid_map) == 0) {
goto G2Q_QUIN;
}
if (k > 0) {
k--;
goto small_id;
}
G2Q5: /* srch from max acceptable ID# */
k = i; /* max acceptable ID# */
G2Q_LP:
m = 1;
m <<= k;
if ((m & assignid_map) == 0) {
goto G2Q_QUIN;
}
if (k > 0) {
k--;
goto G2Q_LP;
}
G2Q_QUIN: /* k=binID#, */
Что надо знать: хотя бы принципы функционального программирования. А в идеале — ООП и архитектурные принципы типа DDD.
Это тот самый код, который имеет очень яркую региональную принадлежность, и если вы аутсорсите, например, в Индию, то встречаться с ним придётся довольно часто.
Копипаста
Сначала были только книги и журналы. Код из них, конечно, можно было перепечатать, но к катастрофам это не приводило. Появился Google (как первый удобный поисковик), и разработчики стали много гуглить. Непонятные куски кода стали появляться в проектах. «Эта штука делает то-то, и не трогайте внутри» стало нормой. Но такими кусками закрывались какие-то дыры, где компетенций не хватало. Появился StackOverflow, и код часто стал состоять из таких скопированных кусков, а разработчики только криво сшивали их между собой. И, наконец, вершиной копипасты стал ChatGPT4 (и аналоги), который умеет сшивать разные фрагменты кода между собой лучше бездумного копирования.
В результате может получиться или прекрасная экономия времени, если вы понимаете каждую строчку кода, или лютейший отборный г*код, который со временем доставит много проблем всем остальным.
И отдельно — про грибы
Это немного не про разработку, но последствия ощущаются и в коде. Есть такая история — «Mushroom management», когда вместо задачи даются требования. Причём частями. По-русски это называется «Сиди в темноте и ешь навоз», что очень точно описывает основные принципы выращивания грибов. Так вот, если так поступать с разработчиками, то вас ждёт море интересного на приёмочных тестах и при интеграциях. Особенно при интеграциях.
Пока всё
В эту статью не попали, например, такие антипаттерны, как Poltergeists, потому что слабо тянет на антипаттерн, Тупик (Dead End), потому что это следствие рассмотренных выше, Непрерывное устаревание и Лодочный якорь, Кладж ввода (Input Kludge) или Слепая вера (Blind faith), потому что он должен автоматически решаться тестами, и другие, которые не относятся напрямую к разработке (или мало распространены с точки зрения автора), но при желании читателей рассмотрим и остальные.
Такие плохие практики называются «антипаттерны» в книге «Банды четырёх» «Шаблоны проектирования», где как раз описываются практики хорошего программирования. Хорошие практики назвали «паттернами», а противоположные — «антипаттернами». Про антипаттерны можно посмотреть ещё вот здесь:
- www.amazon.com/AntiPatterns-William-J-Brown/dp/0471197130
- antipatterns.com (и antipatterns.com/more.htm)
- sourcemaking.com/antipatterns
Ну а я пошёл дальше собирать хорошие практики, чтобы однажды выложить сборку с ними.