Инкрементальная реализация
Обзор
Строй тонкими вертикальными срезами: реализовал кусок — протестировал — проверил — расширил. Не реализуй фичу целиком за один заход. Каждый инкремент должен оставлять систему в работающем и проверяемом состоянии. Это та исполнительская дисциплина, которая делает крупные фичи управляемыми.
Когда применять
- Реализуешь любое изменение, затрагивающее несколько файлов
- Строишь новую фичу по декомпозиции задач
- Рефакторишь существующий код
- Каждый раз, когда тянет написать больше ~100 строк до первого прогона тестов
Когда НЕ применять: изменения в одном файле и одной функции, где объём и так минимален.
Цикл инкремента
┌──────────────────────────────────────────┐
│ │
│ Реализовал ──→ Тест ──→ Проверил ──┐ │
│ ▲ │ │
│ └───── Коммит ◄────────────────┘ │
│ │ │
│ ▼ │
│ Следующий срез │
│ │
└──────────────────────────────────────────┘
Для каждого среза:
- Реализуй наименьший цельный кусок функциональности
- Протестируй — прогони набор тестов (или напиши тест, если его нет)
- Проверь — убедись, что срез работает как задумано (тесты зелёные, сборка успешна, ручная проверка)
- Закоммить — сохрани прогресс с осмысленным сообщением (про атомарные коммиты см.
git-workflow-and-versioning) - Переходи к следующему срезу — двигайся вперёд, не начинай заново
Стратегии нарезки
Вертикальные срезы (предпочтительно)
Строй один полный путь через весь стек:
Срез 1: Создать задачу (БД + API + базовый UI)
→ Тесты проходят, пользователь может создать задачу через интерфейс
Срез 2: Список задач (запрос + API + UI)
→ Тесты проходят, пользователь видит свои задачи
Срез 3: Редактировать задачу (обновление + API + UI)
→ Тесты проходят, пользователь может менять задачи
Срез 4: Удалить задачу (удаление + API + UI + подтверждение)
→ Тесты проходят, CRUD закрыт полностью
Каждый срез даёт работающую сквозную функциональность.
Нарезка от контракта
Когда бэкенд и фронтенд нужно разрабатывать параллельно:
Срез 0: Определить контракт API (типы, интерфейсы, спека OpenAPI)
Срез 1a: Реализовать бэкенд по контракту + тесты API
Срез 1b: Реализовать фронтенд на моках, соответствующих контракту
Срез 2: Интегрировать и протестировать сквозной сценарий
Нарезка от риска
Берись сначала за самый рискованный или самый неопределённый кусок:
Срез 1: Доказать, что WebSocket-соединение работает (наибольший риск)
Срез 2: Построить обновления задач в реальном времени на проверенном соединении
Срез 3: Добавить офлайн-режим и переподключение
Если срез 1 провалится, ты узнаешь об этом до того, как вложился в срезы 2 и 3.
Правила реализации
Правило 0: сначала простота
Прежде чем писать код, спроси: «Что самое простое, что могло бы сработать?»
После того как написал код, прогони его через эти проверки:
- Можно ли сделать это меньшим числом строк?
- Оправдывают ли эти абстракции свою сложность?
- Посмотрит ли на это staff-инженер и скажет ли «а почему ты просто не…»?
- Я строю под гипотетические будущие требования или под текущую задачу?
ПРОВЕРКА НА ПРОСТОТУ:
✗ Универсальная шина событий с конвейером middleware ради одного уведомления
✓ Обычный вызов функции
✗ Паттерн «абстрактная фабрика» ради двух похожих компонентов
✓ Два прямолинейных компонента с общими утилитами
✗ Конструктор форм на конфигах ради трёх форм
✓ Три компонента форм
Три похожие строки кода лучше преждевременной абстракции. Сначала реализуй наивную, очевидно правильную версию. Оптимизируй только после того, как корректность доказана тестами.
Правило 0.5: дисциплина границ
Трогай только то, что требует задача.
НЕ надо:
- «Прибираться» в коде по соседству с твоим изменением
- Рефакторить импорты в файлах, которые ты не меняешь
- Удалять комментарии, которые ты понял не до конца
- Добавлять фичи, которых нет в спеке, потому что они «вроде полезные»
- Осовременивать синтаксис в файлах, которые ты только читаешь
Если заметил что-то стоящее улучшения за пределами своей задачи — зафиксируй, но не чини:
ЗАМЕТИЛ, НО НЕ ТРОГАЮ:
- В src/utils/format.ts есть неиспользуемый импорт (не относится к этой задаче)
- Middleware аутентификации не помешали бы понятные сообщения об ошибках (отдельная задача)
→ Завести под это задачи?
Правило 1: по одной вещи за раз
Каждый инкремент меняет одну логическую вещь. Не смешивай разные вещи:
Плохо: один коммит, который добавляет новый компонент, рефакторит существующий и правит конфиг сборки.
Хорошо: три отдельных коммита — по одному на каждое изменение.
Правило 2: держи проект собирающимся
После каждого инкремента проект должен собираться, а существующие тесты — проходить. Не оставляй кодовую базу сломанной между срезами.
Правило 3: фича-флаги для незаконченных фич
Если фича ещё не готова для пользователей, но инкременты нужно вливать:
// Фича-флаг для незавершённой работы
const ENABLE_TASK_SHARING = process.env.FEATURE_TASK_SHARING === 'true';
if (ENABLE_TASK_SHARING) {
// Новый UI шеринга
}
Так можно вливать мелкие инкременты в основную ветку, не показывая пользователям недоделанное.
Правило 4: безопасные значения по умолчанию
Новый код должен по умолчанию вести себя безопасно и консервативно:
// Безопасно: по умолчанию выключено, включается явно
export function createTask(data: TaskInput, options?: { notify?: boolean }) {
const shouldNotify = options?.notify ?? false;
// ...
}
Правило 5: удобство отката
Каждый инкремент должен откатываться независимо:
- Аддитивные изменения (новые файлы, новые функции) откатываются легко
- Правки существующего кода должны быть минимальными и точечными
- У миграций БД должны быть соответствующие откатные миграции
- Не удаляй что-то и не заменяй это в одном и том же коммите — разнеси на два
Работа с агентами
Когда направляешь агента на инкрементальную реализацию:
«Давай реализуем задачу 3 из плана.
Начни только с изменения схемы БД и эндпоинта API.
UI пока не трогай — сделаем его следующим инкрементом.
После реализации прогони команды тестов и сборки этого репозитория,
чтобы убедиться, что ничего не сломалось.»
Явно проговаривай, что входит и что НЕ входит в объём каждого инкремента.
Чеклист инкремента
После каждого инкремента проверяй командами самого репозитория (см. раздел «Сначала разберись со стеком» в скилле test-driven-development):
- Изменение делает одну вещь и делает её полностью
- Все существующие тесты по-прежнему проходят (команда тестов репозитория:
npm test,./gradlew test,pytest, …) - Сборка успешна (команда сборки репозитория)
- Проверка типов проходит, если в стеке она есть (
npx tsc --noEmit,mypy, …) - Линтер проходит (команда линтера репозитория)
- Новая функциональность работает как задумано
- Изменение закоммичено с осмысленным сообщением
Замечание: запускай каждую проверочную команду после изменения, которое могло на неё повлиять. После успешного прогона не повторяй ту же команду, если код с тех пор не менялся — повторный прогон на неизменившемся коде не даёт новой информации.
Типовые самооправдания
| Самооправдание | Как на самом деле |
|---|---|
| «Протестирую всё в конце» | Баги накапливаются. Баг в срезе 1 делает срезы 2–5 неверными. Тестируй каждый срез. |
| «Быстрее сделать всё сразу» | Это кажется быстрее ровно до момента, когда что-то ломается и ты не можешь найти, какая из 500 изменённых строк виновата. |
| «Эти изменения слишком мелкие, чтобы коммитить отдельно» | Мелкие коммиты ничего не стоят. Крупные коммиты прячут баги и делают откат болезненным. |
| «Фича-флаг добавлю потом» | Если фича не закончена, она не должна быть видна пользователю. Добавь флаг сейчас. |
| «Этот рефакторинг мелкий, включу его сюда же» | Рефакторинг вперемешку с фичами усложняет и ревью, и отладку обоих. Разнеси их. |
| «Прогоню сборку ещё раз, просто чтобы убедиться» | После успешного прогона повтор той же команды ничего не даёт, если код с тех пор не менялся. Запускай её снова после последующих правок, а не для самоуспокоения. |
Тревожные признаки
- Написано больше 100 строк кода без единого прогона тестов
- Несколько несвязанных изменений в одном инкременте
- Расползание границ в стиле «дай я быстренько ещё вот это добавлю»
- Пропуск шага теста/проверки ради скорости
- Сборка или тесты сломаны между инкрементами
- Копятся большие незакоммиченные изменения
- Строятся абстракции до того, как их потребовал третий случай применения
- Трогаешь файлы вне задачи «раз уж я всё равно здесь»
- Создаёшь новые файлы утилит ради разовой операции
- Запускаешь одну и ту же команду сборки/тестов дважды подряд без единой правки кода между ними
Проверка
После завершения всех инкрементов задачи:
- Каждый инкремент был отдельно протестирован и закоммичен
- Полный набор тестов проходит
- Сборка чистая
- Фича работает сквозным сценарием, как описано
- Незакоммиченных изменений не осталось
См. также
Проверка на каждом инкременте — локальный контроль. Прежде чем объявлять задачу выполненной, примени общий для проекта Definition of Done как финальные ворота: это постоянная планка, которую проходит любой инкремент вне зависимости от задачи. См. ../../references/definition-of-done.md.