Выделение репозитория (Extract Repository)
Контроллер заказов пишет SQL сам. За три шага SQL уезжает в шлюз таблицы, а потом за интерфейс репозитория, и контроллер начинает тестироваться без базы.
Исходная задача
Небольшой API заказов: создать заказ, показать его, отменить и показать список заказов покупателя. Отменить можно только новый заказ. Ответ контроллера - Response со статусом и текстом: 201 order 1 created, 404 order 7 not found, 409 order 1 is cancelled, cannot cancel.
Базой служит игрушечная Database в памяти: таблицы - списки рядов, query понимает ровно три формы SQL (SELECT ... WHERE col = ?, INSERT, UPDATE ... SET col = ? WHERE col = ?) и nextId как последовательность. В приложении на её месте PDO, database/sql или драйвер, но для урока важно одно: это внешняя система со строковым языком запросов и рядами без типов на выходе. Код Database одинаковый во всех шагах, меняется только то, что над ней.
Что болит
- В контроллере три слоя сразу: HTTP-ответы, SQL-строки и бизнес-правило "отменить можно только новый заказ". Правило нельзя прочитать, не продираясь через запросы.
- Один и тот же
SELECT * FROM orders WHERE id = ?написан дважды, а имена колонок (total_cents,customer_id) разбросаны по всем методам. - Контроллер работает с рядами: словарь колонка - значение. В Go это
map[string]anyи приведение типов в каждом методе. - Проверить контроллер можно только вместе с базой. Здесь база игрушечная и быстрая, в проекте это контейнер с Postgres и медленные тесты.
План
- Шаг 1: SQL прямо в контроллере - фиксируем поведение тестами, ничего не меняя.
- Шаг 2: шлюз таблицы - все SQL-строки уезжают в
OrdersTable, контроллер зовёт её методы, но всё ещё получает ряды. - Шаг 3: репозиторий в домене - появляется сущность
Orderс правилом отмены и интерфейсOrderRepository; SQL-реализация и реализация в памяти взаимозаменяемы, контроллер тестируется с любой.
Тесты во всех шагах одни и те же: пять сценариев (создать и показать, отказ на нулевую сумму, 404, отмена только нового заказа, список по покупателю) с одинаковыми ожидаемыми ответами. Меняется только строка, которая собирает контроллер. На третьем шаге те же сценарии прогоняются дважды, с каждым репозиторием.
Итог
Контроллер стал переводчиком между HTTP и доменом: достал заказ, попросил его отмениться, сохранил, ответил. SQL живёт в SqlOrderRepository, правило - в Order, а интерфейс OrderRepository говорит на языке задачи: get, save, forCustomer, а не SELECT и UPDATE. Реализация в памяти делает тесты контроллера быстрыми, а общие сценарии для двух реализаций следят, чтобы она не врала.
Второй шаг сам по себе тоже полезен: если правил в домене почти нет и контроллер просто перекладывает ряды в JSON, шлюза таблицы хватает и до репозитория можно не доходить.
Подробнее о приёме: Репозиторий. Как ряды превращаются в объекты, когда маппинг становится сложным: Преобразователь данных.