Спроектируй дважды
Назначение
Сравнить несколько существенно разных решений до реализации. Вы задаёте общие требования, агент готовит альтернативы и показывает их использование, после чего вы выбираете подход по конкретным последствиям для проекта.
Также известен как
Design It Twice, принцип «спроектируй дважды» Джона Аустерхаута.
Проблема
Агент предлагает правдоподобный план, и вы сразу просите его реализовать. Дальнейшее обсуждение уточняет выбранную конструкцию: какие методы добавить, где обработать ошибку, какие тесты написать. При этом само распределение ответственности между модулями остаётся без сравнения с альтернативами.
Например, агент разбивает импорт CSV на чтение, проверку и сохранение строк. Вызывающий код связывает эти шаги и решает, что делать с ошибками. Такой вариант может подходить проекту, но без альтернативы трудно заметить его цену: каждый новый потребитель будет знать порядок действий и правила частичного сохранения.
Когда реализация уже написана, смена конструкции требует переделки кода и тестов. Сравнение небольших эскизов позволяет принять это решение раньше.
Решение
Попросите агента спроектировать минимум два варианта для одной задачи. Задайте обоим одинаковые требования, ограничения и сценарии. Различие должно затрагивать устройство решения: кто управляет процессом, где хранится состояние, что знает вызывающий код.
Для каждого варианта нужны небольшой интерфейс, пример вызова и описание поведения при ошибках. По ним можно увидеть, какие знания остаются у потребителя и какие изменения затронут несколько мест. Общих оценок вроде «гибкий» или «простой» для выбора недостаточно.
Сравните варианты на обычном сценарии и одном-двух трудных случаях. Попросите агента рекомендовать решение и назвать условия, при которых предпочтительнее альтернатива. Зафиксируйте выбор и его основания перед реализацией. Если ключевой вопрос требует наблюдений, выделите для него одноразовый прототип.
Структура
Общие требования расходятся в два эскиза, которые затем сравниваются на одних сценариях.
Варианты могут появиться последовательно в одной сессии. Важна разница в конструкции и общая основа сравнения; число агентов само по себе её не обеспечивает.
Участники / Компоненты
- Разработчик задаёт ограничения и принимает решение с учётом потребностей проекта.
- Агент предлагает альтернативы, показывает вызовы и разбирает компромиссы.
- Общие сценарии позволяют сравнить поведение решений в одинаковых условиях.
- Эскизы описывают интерфейсы, ответственность и ошибки до полной реализации.
- Запись решения сохраняет выбор, причины и условия его пересмотра.
Когда применять
- Проектируется API, которым будут пользоваться несколько модулей.
- При рефакторинге нужно решить, куда перенести ответственность или состояние.
- Первое предложение выглядит убедительно, но его преимущества пока не с чем сравнить.
- Изменение выбранной конструкции позднее затронет много вызывающего кода.
Для локального исправления с однозначным решением сравнение может стоить дороже самой работы. Ограничивайте его конкретным решением, которое влияет на дальнейшую разработку.
Последствия и компромиссы
- ➕ Код использования помогает заметить сложность, которую интерфейс перекладывает на потребителя.
- ➕ Выбор опирается на сценарии и ограничения проекта.
- ➕ Отвергнутый вариант оставляет полезные основания для будущего пересмотра.
- ➖ Подготовка и чтение альтернатив требуют времени.
- ➖ Агент может предложить поверхностные различия или сделать один вариант заведомо слабее.
- ➖ Эскиз не подтверждает производительность и корректность будущей реализации.
Реализация
- Выберите одно решение для сравнения: например, границу модуля импорта.
- Поручите агенту прочитать существующий код и назвать ограничения со ссылками на места в проекте.
- Назовите обычный сценарий и один-два трудных случая, на которых будете сравнивать варианты.
- Запросите два разных распределения ответственности. Ограничьте результат интерфейсами, кодом вызова и описанием ошибок.
- Проверьте, что каждый вариант выполняет все обязательные требования. Если один пропустил требование, верните его на доработку.
- Сопоставьте варианты по коду использования и местам, которые придётся менять. Разберите рекомендацию агента.
- Запишите решение в задаче или ADR и передайте выбранный контракт в реализацию.
Если один агент воспроизводит первый вариант под другими именами, задайте второму подходу явное направление: например, перенесите управление процессом от вызывающего кода внутрь модуля. Можно поручить эскизы отдельным агентам, дав каждому одинаковый исходный контекст и своё направление поиска. Все обязательные требования при этом сохраняются. Для сравнения интерфейсов отдельные ветки с полноценными реализациями обычно не нужны.
Пример
Нужно импортировать пользователей из CSV. Импорт запускают HTTP-обработчик и команда CLI. Файл ограничен 10 000 строками. Корректные строки сохраняются, а для некорректных возвращаются номер строки и причина. Если хранилище недоступно, импорт останавливается; уже сохранённые строки остаются, отчёт содержит их число. Повторный запуск и устранение дублей требуют отдельного решения и в этот пример не входят.
Вы задаёте агенту рамку сравнения:
Спроектируй API импорта пользователей из CSV дважды: в одном варианте шагами управляет вызывающий код, в другом — модуль импорта. Сравни их для HTTP-обработчика и CLI
Ниже — учебные эскизы на JavaScript. Имена функций обозначают предлагаемый контракт; код показывает распределение ответственности и не является готовой реализацией импорта. Для обоих вариантов синтаксическая ошибка CSV останавливает импорт с кодом invalid_csv; ошибка содержимого отдельной строки попадает в отчёт и не мешает остальным строкам.
Вариант A: потребитель управляет шагами. readCsv выдаёт строки с номерами, validateUser возвращает пользователя или список ошибок, users.save сохраняет одного пользователя. При недоступности хранилища save выбрасывает StorageUnavailable.
const report = { saved: 0, rejected: [], stopped: null };
try {
for await (const { line, fields } of readCsv(source)) {
const result = validateUser(fields);
if (!result.ok) {
report.rejected.push({ line, errors: result.errors });
continue;
}
await users.save(result.user);
report.saved += 1;
}
} catch (error) {
if (error instanceof StorageUnavailable) {
report.stopped = "storage_unavailable";
} else if (error instanceof InvalidCsv) {
report.stopped = "invalid_csv";
} else {
throw error;
}
}
Вызывающий код контролирует каждый шаг. Он же знает, когда продолжать обработку, когда останавливаться и как считать сохранённые строки. HTTP-обработчику и CLI понадобятся эти правила. Если вынести весь показанный процесс в общую операцию, граница ответственности приблизится ко второму варианту.
Вариант B: модуль управляет импортом. При создании модуль получает хранилище. Метод run читает CSV, проверяет и сохраняет строки, формирует такой же отчёт. Ожидаемые причины остановки возвращаются в stopped; неожиданные ошибки выбрасываются наружу.
// При сборке приложения:
const userImport = createUserImport({ users });
// В HTTP-обработчике или CLI:
const report = await userImport.run(source);
В обоих вариантах два корректных пользователя и одна некорректная строка дают одинаковый результат:
{
"saved": 2,
"rejected": [{ "line": 3, "errors": ["Некорректный email"] }],
"stopped": null
}
Если первая строка сохранена, а при сохранении второй хранилище стало недоступно, ожидаемый отчёт — saved: 1, rejected: [], stopped: "storage_unavailable". Эти примеры задают контракт обоим вариантам; при реализации их нужно проверить тестами.
| Сценарий или изменение | Вариант A | Вариант B |
|---|---|---|
| Некорректная строка | Потребитель добавляет ошибку в отчёт и продолжает цикл | Модуль возвращает ошибку строки в готовом отчёте |
| Хранилище недоступно | Потребитель останавливает цикл и сохраняет счётчик | Модуль останавливает импорт и возвращает счётчик |
| Добавление второго потребителя | Нужно повторить или вынести правила обработки | Новый потребитель вызывает run |
| Особое действие перед сохранением строки | Потребитель добавляет его в свой цикл | Нужно изменить модуль или расширить его контракт |
Для заявленной задачи выбираем B: HTTP и CLI используют одинаковые правила импорта, поэтому полезно держать их в одном месте. Вариант A имеет смысл, если потребителям нужны разные последовательности обработки. Короткая запись решения сохраняет это условие:
Выбрали модуль с операцией run: он владеет обработкой строк и формированием отчёта для HTTP и CLI. Корректные строки сохраняются независимо; ожидаемый сбой останавливает импорт с отчётом о частичном результате. Пересмотрим границу, если потребителям понадобятся разные процессы обработки.
Антипаттерны и частые ошибки
- Два названия одного решения. Классы и функции переименованы, но ответственность осталась прежней. Попросите показать, какое знание о процессе переехало в другой модуль.
- Неравные условия. Один вариант решает только успешный сценарий, а другой обрабатывает ошибки. Сначала доведите оба до общих требований.
- Сравнение по числу методов. Один метод может скрывать десятки обязательных настроек и сложный порядок подготовки. Рассматривайте весь код использования.
- Полная реализация каждого варианта. Объём работы растёт до того, как сформулирован вопрос, требующий запуска. Начните с эскизов и выделяйте эксперименты отдельно.
- Бесконечный перебор. Новые варианты появляются без нового критерия выбора. Завершайте сравнение, когда требования покрыты, а существенные компромиссы понятны.
- Автоматическое принятие рекомендации. Агент может неверно оценить будущие потребности проекта. Проверьте допущения, на которых держится выбор.
Известные применения
- Джон Аустерхаут рассматривает Design It Twice в книге A Philosophy of Software Design; принцип также включён в материалы его курса CS 190. Здесь он адаптирован к работе разработчика с агентом.
- Скиллы Мэтта Покока содержат Design It Twice внутри codebase-design: несколько агентов проектируют разные интерфейсы, показывают их использование и сравнивают, какую сложность скрывает модуль и где будут сосредоточены изменения. Это вариант организации работы; описанный в главе процесс можно выполнить и в одной сессии.
Связанные паттерны
- Гриллинг проверяет допущения готового плана; сравнение альтернатив помогает выбрать саму конструкцию.
- Одноразовый прототип даёт наблюдения для вопросов, которые остались после сравнения эскизов.
- Четыре фазы отводит место для сравнения перед реализацией, на этапе планирования.
- Словарь домена помогает использовать общие термины в вариантах и сохранить принятое решение в ADR.