Глубинные преступления против проекта
Jun. 8th, 2023 05:06 pmОдно из самых сложных и повреждающих действий в проекте - повышение сложности проекта без должной отдачи.
Когда человек сталкивается с новым требованием в проекте, он открывает код, ищет, куда что дописать. Находит. Дописывает. Фича готова. Если проект достаточно покрыт тестами, то старые фичи/юскейсы не сломаны. Задача сделана, можно сказать, на отлично.
... Но в глубине...
В каждом месте, где он дописал, изменение, грубо говоря, можно свести к:
* всегда делать что-то (добавлять timestamp к сообщению). Сложность не изменилась, точнее, изменилась на несколько строчек. С точки зрения необоснованной сложности вопросов нет. Могут быть вопросы по дублированию кода (если одна и та же строчка получения/форматирования времени появилась в нескольких местах).
* Делать что-то по условию (если у нас есть фичефлаг 'timestamp', добавлять timestamp). Был линейный код, теперь у нас ветвление. Маленькое или большое - но ветвление. В декартовом произведении состояний приложения всё удвоилось.
* Делать что-то по условию в ветвях условий. Если это одно и то же, то это просто дублирование кода и предыдущий случай, если же это разные действия, то мы получаем нарушение инварианта, в котором, возможно, есть не покрытые тестами ветки.
* Добавить трекинг фичефлага или "режима", или "глобальной переменной". Теперь весь (или существенная часть) кода параметризирована, и, возможно, имеет необходимость отвечать на вопрос "что делать в этой ситуации" даже там, где фича не нужна или не имеет смысла.
* Вызывать распространяющиеся изменения (например, фичефлаг - линейный код в месте использования, но новый код вызывает расползающиеся изменения в компонентах, которые зависили от старого кода, включая "накладывание" эффектов идущих по разным ветвям кода на другой код, который вынужден вместо одного "да/нет" уметь реагировать на комбинацию действий разных ветвей кода (А: да, Б: нет; А: да, Б: да, А: нет, Б: да, А: нет, Б: нет).
* Вызвать появление нескольких версий интерфейсов или нарушение контрактов интерфейсов из-за появления вариантивности из-за фичи, включая необходимость думать о ковариантности или контр-вариантности (паталогический случай: когда в код передают сущность (коллбэк, замыкание), ожидающую объект с фичей или без фичи как параметр); особо тут обстоит вопрос с контрактами для внешних потребителей.
* Вызывать нелинейные сайд-эффекты, проявляющиеся через нетипизированные изменения среды исполнения (блокировки из-за чрезмерно активной записи, переполнения, etc).
И всё это выглядит как feature done, но последствия для проекта совершенно разные.
А вот заметить это глубинное преступление очень трудно. Feature done? Ну да, пришлось добавить matrix, ну да, мы теперь с помощью if'а вычисляем зависимости вместо линейного списка, у нас появилась ещё одна переменная, которую по Глубокой Причине надо выставлять в явном виде и помнить про её существование всем остальным вокруг.
Если это всё немного обобщить, то преступления тут нескольких видов:
* Повышение цикломатической сложности
* Нарушение инвариантов
* Добавление паталогических связей (с особым преступлением, когда оказываются связаны вместе два компонента, которые раньше были независимы)
* Экспозиция сложности в контракты
* Повышение нестабильности сайд-эффектов.
... И я не имею ни малейшего представления как это можно заметить, кроме как на интуиции, и то, не всегда.
Когда человек сталкивается с новым требованием в проекте, он открывает код, ищет, куда что дописать. Находит. Дописывает. Фича готова. Если проект достаточно покрыт тестами, то старые фичи/юскейсы не сломаны. Задача сделана, можно сказать, на отлично.
... Но в глубине...
В каждом месте, где он дописал, изменение, грубо говоря, можно свести к:
* всегда делать что-то (добавлять timestamp к сообщению). Сложность не изменилась, точнее, изменилась на несколько строчек. С точки зрения необоснованной сложности вопросов нет. Могут быть вопросы по дублированию кода (если одна и та же строчка получения/форматирования времени появилась в нескольких местах).
* Делать что-то по условию (если у нас есть фичефлаг 'timestamp', добавлять timestamp). Был линейный код, теперь у нас ветвление. Маленькое или большое - но ветвление. В декартовом произведении состояний приложения всё удвоилось.
* Делать что-то по условию в ветвях условий. Если это одно и то же, то это просто дублирование кода и предыдущий случай, если же это разные действия, то мы получаем нарушение инварианта, в котором, возможно, есть не покрытые тестами ветки.
* Добавить трекинг фичефлага или "режима", или "глобальной переменной". Теперь весь (или существенная часть) кода параметризирована, и, возможно, имеет необходимость отвечать на вопрос "что делать в этой ситуации" даже там, где фича не нужна или не имеет смысла.
* Вызывать распространяющиеся изменения (например, фичефлаг - линейный код в месте использования, но новый код вызывает расползающиеся изменения в компонентах, которые зависили от старого кода, включая "накладывание" эффектов идущих по разным ветвям кода на другой код, который вынужден вместо одного "да/нет" уметь реагировать на комбинацию действий разных ветвей кода (А: да, Б: нет; А: да, Б: да, А: нет, Б: да, А: нет, Б: нет).
* Вызвать появление нескольких версий интерфейсов или нарушение контрактов интерфейсов из-за появления вариантивности из-за фичи, включая необходимость думать о ковариантности или контр-вариантности (паталогический случай: когда в код передают сущность (коллбэк, замыкание), ожидающую объект с фичей или без фичи как параметр); особо тут обстоит вопрос с контрактами для внешних потребителей.
* Вызывать нелинейные сайд-эффекты, проявляющиеся через нетипизированные изменения среды исполнения (блокировки из-за чрезмерно активной записи, переполнения, etc).
И всё это выглядит как feature done, но последствия для проекта совершенно разные.
А вот заметить это глубинное преступление очень трудно. Feature done? Ну да, пришлось добавить matrix, ну да, мы теперь с помощью if'а вычисляем зависимости вместо линейного списка, у нас появилась ещё одна переменная, которую по Глубокой Причине надо выставлять в явном виде и помнить про её существование всем остальным вокруг.
Если это всё немного обобщить, то преступления тут нескольких видов:
* Повышение цикломатической сложности
* Нарушение инвариантов
* Добавление паталогических связей (с особым преступлением, когда оказываются связаны вместе два компонента, которые раньше были независимы)
* Экспозиция сложности в контракты
* Повышение нестабильности сайд-эффектов.
... И я не имею ни малейшего представления как это можно заметить, кроме как на интуиции, и то, не всегда.
no subject
Date: 2023-06-12 10:55 am (UTC)Ну, на самом деле говорить "код плохой" для кода, который уже в продакшене, это ошибка. Он может не подходить под новую задачу или не соответствовать новым требованиям, но в целом, если речь идёт не про нарушение старых требований, код хороший, если работает.
... Я для себя давно поменял модель отношения к коду: когда ты работаешь со старым кодом для новых требований, разумеется, он не подходит к новым требованиям, и требуется адаптация (на самом деле в обе стороны - требования тоже не константные и могут подвинуться, если очень нужно). То есть вместо "ужас-ужас-ужас", "у нас новые требования и они расходятся со старым кодом".
no subject
Date: 2023-06-12 10:57 am (UTC)Это ж были тесты. Которые частенько ничего вообще не рапортовали. Ну как перегоревшая сигнальная лампочка - и в продакшене, и рапортует успех.
no subject
Date: 2023-06-12 10:59 am (UTC)И к тестам это тоже относится, они проверяли в одной реальности, а сейчас реальность уже другая. Тесты, кстати, по своим требованиям гниют даже быстрее кода, потому что человек закрывал одну проблему, а потом проблему убрали, и теперь тест либо не то проверяет, либо всегда успех, потому что поломать не может.
... И отдельная религия - это тестовые остнастки (фикстуры и т.д.). Они с одной стороны гниют из-за изменений продакшена, с другой стороны создают свои требования к тестам (которые вовсе не очевидны), с третьей их всё время норовят в тестах использовать "не так, как задумывалось"...