Pull to refresh

Comments 14

Сергей, у меня к вам пара вопросов:
1. Для чего вы запускаете программы, разработанные на Go, в контейнерах?
2. Разве о таком именовании пакетов (классов, модулей) говорил Роберт Мартин? Я имею в виду «Домен», «Приложение», «Транспорт». У него, насколько я помню, каждый класс или пакет должен своим названием говорить что именно он делает. Типа «Логирование», «Ошибки», «Отчётность».
Кажется, что здесь используется структура проекта, характерная для проектов с гексагональной архитектурой.

Не соглашусь. В статье Clean Architecture есть ссылка на статью Hexagonl Architecture (статья уже недоступна, смотрел на archive.org). В статье про Hexagonal Architecture нет конкретных правил именования, там вообще нет привязки к языкам программирования. При реализации Hexagonal Architecture, Clean Architecture, DDD и других решений команды либо просто не понимают и игнорируют правила, о которых они вроде как читали (такое приходилось видеть :D), либо у них возникает очень много практических вопросов, на которые первоисточник ответа не даёт (или даёт, но абстрактный, на уровне требований).


В итоге люди сами выбирают, как решать конкретные вопросы, соблюдая Hexagonal Architecture, Clean Achitecture и другие подходы. Где-то идут на компромиссы, где-то смотрят на опыт других, где-то изобретают сами. Примеров много на github: roblaszczak/go-cleanarch, apavamontri/nodejs-clean, CodelyTV/cqrs-ddd-php-example.


Кстати, есть пример с кричащими названиями всех пакетов: AkbaraliShaikh/denti. Мы так тоже пробовали когда-то, и отказались: да, все пакеты кричат о своём назначении, но они все свалены в кучу независимо от уровня — визуально трудно отделить более важные пакеты бизнес-логики от менее важных пакетов более близкого к вводу-выводу уровня.

К сожалению практика показывает, что очень многие люди называют модули именно так как они привыкли делать это на тех технологиях, с которых они перешли на Go. Поддерживаю замечание автору.
  1. Контейнер более переносим и легче совмещается с другими инструментами, чем даже статический бинарник на Go. В частности, мы используем Kubernetes для оркестрации Docker-контейнеров (как минимум — для раскатывания переменного числа реплик сервисов на более постоянное число машин). Мы используем docker-compose для локальной разработки — одним docker-compose файлом поднимается и приложение, и MySQL/redis. Мы используем Docker даже для сборки наших проектов (что даёт предсказуемое и легко переносимое окружение сборки) и — в некоторых случаях — для запуска консольных приложений.

2) Роберт Мартин пишет абстрактно, у него нет конкретных правил по именованию пакетов Go, есть абстрактный совет по именованию компонентов приложения: при чтении кода должно быть понятно, что это такое. Наша команда опробовала несколько вариантов и в итоге пришла к тому, что "кричащее" название имеет компонент верхнего уровня, а внутри него уже находятся каталоги "app", "domain", "infrastructure". Поскольку infrastructure скрывает за собой несколько вещей, внутри него есть пакеты с кричащими названиями — но эти названия кричат о технологиях, поскольку о своём назначении инфраструктурный пакет ничего не знает (он реализует интерфейс, не особо зная, как им пользуются). В примере компонент верхнего уровня один — "cooking", также мы вводим компонент "common" где, например, размещается инфраструктурный код для подключения к БД. В простых микросервисах, выполняющих одну задачу, так и остаётся один компонент. В более сложных сервисах — компонентов может быть много, их названия говорят об их назначении.


Есть идея у меня лично transport переименовать в grpc. Что касается логирования и обработки ошибок — у нас это делается как Middleware, реализующий интерфейс, сгенерированный из proto-файла GRPC-плагином для protoc. Так что выделять их в другой пакет нет особого смысла, эти Middleware завязаны на GRPC.


От именования пакетов способность кода "кричать" о своём назначении не страдает — новичок обычно читает код, начиная с main, и там импортируется пакет .../pkg/cooking/app, в пути к которому есть строка, описывающее назначение. Прямо в main вызывается NewLoggingMiddleware или NewErrorHandlingMiddleware, где название опять же говорит о назначении middleware.

От именования пакетов способность кода «кричать» о своём назначении не страдает


Вот тут позволю себе не согласиться. Тем более что про именование (пакетов, переменных и пр.) написано очень многое и членами команды разработки Go и известными в сообществе людьми. И их рекомендации не ложатся на вашу схему, что, впрочем не отменяет возможности того, что вы правы в вашем конкретном случае.
А Дядя Боб вообще писал хоть и абстрактно, но из его примеров явно торчали уши Java и RoR. Так что его рекомендации нужно применительно к Go здорово фильтровать, в отличие от рекомендаций того же Роба Пайка. Это моё мнение.
Про контейнеры ответ прочитал. Есть мнение, что несмотря на описанные здравые аргументы, у вас тоже присутствует элемент моды и ажиотажа на них. Я как, человек много и плотно поработавший с различной виртуализацией самой разной инфраструктуры, откровенно рад что для проектов на Go можно без всего этого обойтись.

А как строите развёртывание и масштабируете разработку без контейнеров? Какие потребности и какие решения?

А как строите развёртывание и масштабируете разработку без контейнеров? Какие потребности и какие решения?


От вопроса веет какой-то безысходностью :)
Контейнеры — лишь один из способов виртуализации со своими плюсами и минусами. Ну да, я тоже использую виртуализацию, но, как правило, не контейнерную.
Но суть не в этом. Скомпилированный Go-бинарник — это и есть контейнер со всеми необходимыми зависимостями, вполне себе самодостаточный. В этом ключе смысла засовывать его в ещё один контейнер я не вижу.
Как масштабировать? Да точно так же — либо вы увеличиваете мощности виртуальной машины или железного сервера и масштабируетесь вертикально, либо запускаете несколько экземпляров в разных машинах или серверах и масштабируетесь горизонтально. Тут ничего нового.
Вы ещё запускаете СУБД в контейнере — это, видимо, единственная причина использования контейнера как среды (реальная). Тут дело, конечно, хозяйское, но я против баз данных в контейнерах. Контейнеры — они больше про удалил/развернул/добавил.
Развёртывание у меня при помощи TeamCity. Всё просто: скачали репо, скомпилировали, подменили бинарник, перезапустили. Вы, полагаю, примерно то же самое делаете с контейнером. У меня на одну сущность меньше.
Добавлю по поводу именования пакетов.
Предложенному вами именованию следует go-kit. Такое решение хорошо подходит для маленьких сервисов. Мы попробовали использовать этот подход, но приложение росло, и в какой-то момент в корне стало слишком много пакетов и стало сложно разобраться, что к какому уровню относится.
При этом в книге Вон Вернона «Implementing Domain-Driven Design» используется именование пакетов именно «domain», «application», «infrastructure».
Плюс, и в чистой архитектуре, и в iddd говорится, что они не свод правил, а набор рекомендаций. Потому в каждом конкретном решении необходимо находить свои конкретные компромисы :)
Про способы логирования могу добавить своё мнение, что очень полезно писать в системный журнал название файла и строчку кода, где был вызов логера. Делается это примерно так:
_, file, line, _ := runtime.Caller(1)

Параметр в функции означает глубину вызова по стеку.
Если программист хочет писать логи, он должен получать извне интерфейс Logger, причём делать это следует на уровне инфраструктуры, а не app или domain.

А как логируете доменный слой? Или вообще не логируете?

Не видим причин логировать в доменном слое. Любые действия сервиса проявляются в пакетах, участвующих в вводе и выводе, будь то API сервиса, слой работы с БД, публикация сообщений или прямое взаимодействие с другими сервисами. То есть можно логировать на уровне infrastructure.
Если потребуется особое ведение логов со слоя app — то скорее всего добавим интерфейс, и реализуем его на уровне infrastructure. И назовём без слова log, например, ActionReporter.
Я так сделал в консольной утилите — на уровне app используется интерфейс Reporter, где каждый метод сообщает одну конкретную вещь (например, метод WarnBadCommit пишет о "плохом" коммите git, вызывается раза 3 с разными текстами). На уровне infrastructure пакет console объявляет структуру console.reporter и функцию console.NewReporter. Структура реализует запись в консоль — через logrus в моём случае, я использовал logrus для форматирования вывода в консоль, абстракцию log.Logger не применял т.к. вывод консольной утилиты — это не логирование.
Если именно со слоя domain — то лучше будет в domain только порождать доменные события и отправлять их в какой-нибудь интерфейс EventDispatcher, а уже в реализации интерфейса на уровне infrastructure (может быть в Middleware) логировать.
Это моё мнение по задаче, которую я на практике не решал, могут быть и другие пути решения.

sergey_shambir, cпасибо большое за статью! Особенно UnitOfWork такой простой оказался, объединил его с go-pg и радуюсь как дитя:)
Sign up to leave a comment.

Articles