Шаг 1: SQL прямо в контроллере
Исходный код: контроллер заказов сам пишет SQL, разбирает ряды и решает, можно ли отменить заказ.
Урок: Выделение репозитория. Следующий шаг: шлюз таблицы.
Что было
OrdersController получает Database и в каждом методе делает всё сам. create проверяет сумму, берёт nextId('orders') и пишет INSERT. show и cancel начинают с одного и того же SELECT * FROM orders WHERE id = ?. cancel смотрит на колонку status и решает, можно ли отменять, а потом пишет UPDATE. listByCustomer выбирает ряды по customer_id и склеивает их в строку.
Database - игрушка в памяти, которая понимает три формы SQL. Она одинаковая во всех шагах урока и в рефакторинге не участвует.
Что меняем
Пока ничего. Это точка отсчёта: код работает, и сначала его поведение фиксируется тестами. Тесты обращаются только к публичным методам контроллера и сверяют Response целиком, статус и текст. О SQL и рядах они ничего не знают, поэтому переживут любой рефакторинг под контроллером.
Сценариев пять: созданный заказ виден в show, нулевая сумма даёт 422, несуществующий заказ даёт 404 и в show, и в cancel, отменённый заказ второй раз не отменяется (409) и показывается как cancelled, список покупателя содержит только его заказы.
Как это собирается
Контроллер получает базу в конструкторе, больше ему ничего не нужно. В примере запросы идут один за другим, как их прислал бы роутер: два заказа, отмена первого, повторная отмена, список.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Что стало лучше
Ничего, но у этого кода есть честное достоинство: каждый метод читается сверху вниз, и видно, какой запрос уходит в базу. Для CRUD без правил так пишут часто, и это нормально.
Что ещё болит
- Правило "отменить можно только новый заказ" спрятано между
SELECTиUPDATE. Его нельзя переиспользовать в другом месте, например в фоновой задаче, без копирования SQL. - Запрос по
idнаписан дважды, имена колонок повторяются в каждом методе. Переименовать колонку - пройти по всему контроллеру. - Контроллер знает форму ряда. В Go это видно лучше всего:
rows[0]["status"].(string)и отдельная ветка на случай, если там не строка. - Тест контроллера невозможен без базы. Подменить её нечем: контроллер зависит от конкретной
Databaseи от её языка запросов.
Запуск: make test T=lessons/002-extract-repository/step-1