Comments 7
Код несколько упрощен (может это и out of scope для gokit, но как на практике без этого в реальном проекте непонятно):
1. обычно в БД содержатся еще поля, которые не хочется выдавать наружу (по крайней мере всегда). Поэтому интересно посмотреть GetByIDResponse, который содержит только часть ордера. Hint: в Java для этого есть mapstruct.org, интересно как это реализуется в Go / Gokit.
2. хотелось бы так же посмотреть пример валидации данных запроса и подготовка модели для сохранения (только часть полей модели БД может задаваться в моделе запроса), т.к. это опять же обычно обязательная вещь.
В репозитарии, наверное, указан некорректный метод ChangeOrderStatus: обычно репозитарий валидирует только возможность сохранения модели (например, что строка по длине помещается в ячейку БД), но не контролирует корректность переходов между состояниями — это функция сервисов. Поэтому, обычно, просто метод сохранения, без ограничения на только изменения статуса. Хотя и так можно, особенно, если реализация такого метода генерируется автоматически.
Из интересного можно отметить, что в Spring обычно нет отдельного слоя Endpoint: там связка протокола и сервиса в одном классе. Так же довольно многословно получается в Endpoint и Transport: цена за отсутствие «магии».
Ну это перевод статьи, поэтому на некоторые вопросы я наверное не смогу ответить.
То что есть плата в виде многословности с Endpoint и Transport за отсутствие "магии" — здесь я с вами полностью согласен. Но эту многословность можно решить, например, с помощью кодогенерации.
Остерегайтесь синдрома Spring Boot Mikroservices
Но завязывание на любой фреймворк, который вносит достаточно много своего — это почти тот же самый синдром, только в профиль
Я Go kit пока не использовал — хотелось его пощупать, но случай пока не представился. Возможно, некоторые моменты из статьи не являются недостатками Go kit, а являются особенностями использования Go kit автором.
бизнес логики, которая ничего не знает о используемом транспорте, вы свободны использовать различные транспорты для одного и того же сервиса
…
Бизнес логика в сервисном слое содержит основное ядро бизнес логики, которая не должна знать ничего о используемых эндпоинтах или конкретном транспортном протоколе
Я очень люблю RPC, но помимо HTTP/1, REST и RPC есть немало других протоколов, которые не вписываются в простую схему запрос/ответ (я вот даже не уверен, что HTTP/2 вписывается, напр. тот же server push). В таких случаях бизнес логика нередко должна знать специфику транспорта, чтобы работать достаточно эффективно. Например, если речь о потоке событий, то бизнес логика зачастую должна быть написана в стиле отдельной горутины, которая отправляет события в канал, и которой можно управлять через сигналы — если обернуть эту бизнес логику в набор RPC методов ради формального требования "бизнес логика не знает о транспорте" то код станет заметно сложнее, а работать будет заметно хуже. Доступные сигналы, которые нужны для управления бизнес логикой, тоже нередко определяются транспортным протоколом. В общем, отрывать бизнес логику от транспорта это хорошая идея, но не везде применимая.
Как пример, один сервис написанный на Go kit может одновременно предоставлять доступ к нему по HTTP и gRPC.
Это здорово, но… на практике такая необходимость практически не встречается. Я к тому, что это явно не та фича, которой стоит рекламировать инструмент.
type Order struct
Хм. Если бизнес логика ничего не знает о транспортном уровне и типе кодирования данных при передаче по сети — зачем поля структуры описывающей данные домена размечены тегами для кодирования в json?
func NewService(rep ordersvc.Repository, logger log.Logger) ordersvc.Service {
Сохранение логгера внутри сервиса обладает серьёзным недостатком, который наглядно демонстрирует следующая строка:
logger := log.With(s.logger, "method", "Create")
Логирование, как правило, должно быть контекстно-зависимо от обрабатываемого запроса: нужно выводить с какого IP пришёл запрос, от имени какого юзера/с какими правами выполняется, ну и да, вызванный метод нужно тоже выводить, причём очень хочется чтобы за это отвечала одна строка кода в слое работы с RPC, и не надо было добавлять лишнюю строку в каждый метод бизнес логики (да ещё и вписывать в неё имя метода вручную — это гарантирует наличие опечаток, особенно учитывая что такие строки будут добавляться исключительно копипастом).
Всё это означает, что логгер необходимо настраивать в соответствии с текущим запросом в слое RPC, и передавать в методы бизнес логики параметром (в крайнем случае — внутри параметра ctx).
type CreateRequest struct {
Order order.Order
}
Волшебно. Т.е. если у нас меняются данные бизнес логики то это автоматически изменит данные бегающие по RPC. Ну да, совместимость API это для слабаков.
return CreateResponse{ID: id, Err: err}, nil
Если ошибку надо возвращать вторым значением, почему она перемещается в результат? Если нужно различать ошибки бизнес логики и транспортные — откуда в этой части кода вообще может взяться транспортная?
Резюмируя: каждый раз, когда я читаю что-то по Go kit, у меня возникает впечатление, что он реализует гибкость немного не в том месте, где она реально нужна. Может дело не в Go kit, а в том как им пользуются, но вообще тенденция немного настораживает.
Весьма интересные замечания и предложения.
В общем, отрывать бизнес логику от транспорта это хорошая идея, но не везде применимая.
С этим пунктом полностью согласен, тут не будет "серебряной пули".
нужно выводить с какого IP пришёл запрос, от имени какого юзера/с какими правами выполняется, ну и да, вызванный метод нужно тоже выводить, причём очень хочется чтобы за это отвечала одна строка кода в слое работы с RPC,
Не всегда сервис бывает верхнеуровневым, чтобы в нем можно было отследить данные по IP и прочее, я думаю этот кейс лучше будет решаться с помощью сквозного ID для запроса.
Волшебно. Т.е. если у нас меняются данные бизнес логики то это автоматически изменит данные бегающие по RPC. Ну да, совместимость API это для слабаков.
Наверное в случае с применением паттерна микросеврис, будет проще поддерживать активными 2 экземпляра сервисов с разным API и делать роутинг запросов на них.
Резюмируя: каждый раз, когда я читаю что-то по Go kit, у меня возникает впечатление, что он реализует гибкость немного не в том месте, где она реально нужна. Может дело не в Go kit, а в том как им пользуются, но вообще тенденция немного настораживает.
Отдать должное, я в целом поддерживаю подход автора статьи, но с удовольствием посмотрел бы на другой способ применения Go kit при разработке миркосервисной архитектуры. Хотя может в случае элегантного монолита проблем описанных выше, было бы меньше.
PS
powerman если у вас есть опыт разработки микросервисов с помощью какого либо toolkit или framework, а может быть и без, могли бы им поделиться?
решаться с помощью сквозного ID для запроса
Когда логи глазами читаешь то сквозной ID не очень удобен, хочется в каждой строке видеть подробности. Если логи просматриваются через специальные утилиты — тогда да, можно обойтись ID. Но в контексте моего комментария разницы между ID и остальными деталями нет — в любом случае ID определяется при получении запроса, на уровне протокола, и логгер в котором этот ID прошит должен быть передан параметром в методы бизнес логики.
Кроме того, следующие по цепочке сервисы не всегда выполняются с правами и в контексте оригинального запроса, плюс по цепочке накапливаются и другие данные — таймауты, стек вызовов, etc. Так что одним ID обойтись обычно не удаётся.
проще поддерживать активными 2 экземпляра сервисов с разным API и делать роутинг запросов на них.
И чем это поможет если второй в цепочке сервис постоянно ломает своё API (и, соответственно, ломает этим первый сервис)? Нет, эта проблема решается намного проще, хотя и требует написания некоторого количества относительно тупого кода: структура Order которую возвращает API не должна быть той же структурой Order, с которой работает бизнес логика. Это значит, что при передаче данных между слоями API и бизнес логики требуется постоянно конвертировать данные между этими двумя структурами в обе стороны. Такой код раздражает (кодогенераторы немного помогают), но зато мы имеем стабильное API и одновременно возможность почти как угодно менять данные бизнес логики.
Я свои сервисы пишу обычно на стандартной библиотеке плюс jsonrpc2 для RPC и structlog для логирования. Middleware обычно нужны при использовании HTTP, и они элементарно пишутся на стандартной библиотеке, либо в виде функций func SomeMiddleware(next http.Handler) http.HandlerFunc
либо в виде генератора такий функций type Middleware func(http.Handler) http.Handler; func makeSomeMiddleware(options) Middleware
.
Микросервисы на Go с помощью Go kit: Введение