Pull to refresh

Comments 50

PinnedPinned comments

Привет! Я долго тружусь над статьями, но некоторыми мыслями или наблюдениями хочется делиться в более быстром и доступном формате, что-то хочется обсудить или узнать у более опытных людей)

Не знаю на сколько можно делиться ссылками у себя в комментариях, но я решил, что хочу начать вести свой ТГ-канал и делиться мыслями там: https://t.me/siliconchannel. Надеюсь никого не смущает такая самореклама в комментариях к своей статье, надеюсь кому-то может быть это интересно

Спасибо, за статью с предложенным видением. Архитектура - это хорошо, но лично мне не хватает еще эту архитектуру увидеть в разрезе написания тестов (unit + интеграционные тесты).

type UserRouter struct {
...
logger         logger.Logger
...
}

Логгер - это тоже как бы middleware. Почему бы все остальные MW тоже к роутеру не прикрутить сразу?

вообще нет. логгер - это логгер. то, что пишет логи в Io.Writer. Его можно в мидлварь покинуть, можно в хэндлер.

Если вы решите поменять базу данных

Почему это так часто рассматривается? Я ещё понимаю когда в систему сразу закладывается поддержка разных СУБД. Но вот это мифическое "если мы захотим переехать на другую СУБД" - почему в ответ на такое пальцем у виска не крутят?

Микросервисы могут дорасти до постгре с склЛайт, или деграднуть )
Мокать проще
В интеграционных тестах можно подсунуть склЛайт
Хороший интерфейс - лучше доки)

Не будем же мы оспаривать букву Д из СОЛИДА дядя Боб накажет)

Так делать не надо, интеграционные тесты они на то и интеграционные, что проверяют реальные связи между сервисами на стенде , и в них база должна та же что и на проде, чтобы отлавливать всякие баги типа реконнектов, concurency и прочее

да, возможно это самый слабый из аргументов, но по-моему странно спорить относительно пользы интерфейсов, особенно в го

для моканья в юнит-тестах есть sqlmock. Для интеграционных надо использовать тот же движок БД, что и будет дальше использоваться, а не подменять его на другой, потенциально ловя различные артефакты "потом"

Как по мне, особо нет смысла мокать sql запросы так как в них нет ни какой сложной логики, в большинстве случаях это просто выполнить запрос и смаппить ответ.

при использовании какого-нить ORM, позволяет контролировать, что запросы приходят в БД те, что ожидались и что при очередном обновлении ORM, запросы не изменились.
А так да, можно и через mock репозитория юниты гонять

вероятно вы решите "поменять базу данных" на стадии тестирования

Зачем вам тестить на другой бд?) Для тестов надо наоборот придерживаться конфигурации максимально близкой к проду, чтобы снизить количество ошибок.

для тестирования юнитов, зависящих от репозитория, мне не нужен реальный репозиторий, потому что я тестирую не репозиторий.

так замокайте слой репозитория и тестируйте без привязки вообще к СУБД

Одна из ключевых идей хорошей архитектуры — это изоляция изменений. Если вы решите поменять базу данных, драйвер для работы с ней или даже библиотеку для логирования, это не должно превращаться в кошмар, где приходится переписывать половину проекта. Чтобы избежать этого, мы используем интерфейсы. Интерфейсы позволяют абстрагироваться от конкретной реализации и легко менять её в будущем.

а тут о чем? вы поменяли слой репозитория на замоканную версию. Мы разве интерфейсы и абстракции не в том числе для тестирования пишем?

Смена субд взята абстрактно, как более наглядный пример. На практике такие ситуации случались дважды:
- Переезжали на новый логгер
- Разрабы ломали обратную совместимость и при переезде на новую версию надо было все перелопачивать (Привет tarantool)

Опыт работы в стартапах и студиях показал, что ситуации из разряда "мы хотим постгрес, ой, то есть монгу, ой, то есть давайте склайт" очень частое явление. И проще один раз написать пару абстракций и интерфейсов, от которых можно будет построить адаптеры хоть для записи в файл, нежели каждый раз идти и править все зависимости в коде.

Мы мигрировали как-то с MySQL на MongoDB. Правки были только в репозитории.

значит вы не использовали ни MySQL ни MongoDB в их полную силу

Ну вообще ништяк!
Спасибо!
Я понимаю что это базовая история и каждый проект, в какой-то степени уникальный, но есть вопросы:
1. Не много ответственности у адаптера(а если данные нужно как то мутировать, сервису нельзя это доверять, он не должен знать о том что там ждут на другом конце?)
2. Он связан с предыдущим, если мы в качестве транспорта используем gRPC, а не REST адаптер должен брать на себя маппинг данных во время запроса и ответа?

В любом случае статья в закладочки)

Привет! Спасибо за крутой фидбек)

По сути, назначение адаптера сводится к хендлеру, только в обратном порядке. Если в хендлере мы получаем запрос, обрабатываем его и отдаем ответ, то в адаптере мы обрабатываем данные, отправляем запрос и затем обрабатываем ответ. Так что, отвечая на вопросы:

  1. В принципе, ответственности не сильно больше, чем у хендлера: обрабатывать и отправлять/получать данные.

  2. Исходя из первого, маппинг можно отнести к обработке данных, так что да, вполне логично сделать его на этом уровне.

А где в этой схеме живут транзакции? Существует масса ситуаций, при которых в рамках одного http запроса надо выполнить более, чем одну операцию на запись в базу и при этом сущности которые мы хотим менять разные, а значит репозитории разные, юзкейсы разные. В каком слое стартует транзакция, в каком завершается, где хранится указатель на базу, с которого стартует транзакция?

Тут смотреть паттерны - 2PC / Saga / Event sourcing - вопрос выходит за рамки данной базовой архитектуры

Ради чего вы хотите применять двухфазный коммит вместо обычной sql транзакции?

ну вообще, чисто теоретически можно прям в репозиторий закинуть мьютекс с транзакцией, единственное что придется каждый раз в юзкейсах делать по 2 дополнительных вызова открытия / закрытия транзакции. но на Go возможно есть более изящный способ с этим выкрутится, сам на нем почти не писал

Не совсем понял, что конкретно вы предлагаете. В рамках одной транзакции мне надо вызвать 2 обновления у двух разных репозиториев, у каждого из них свой юзкейс. Единственный выход, который вижу я, это на уровне хэндлера создавать транзакцию и далее её прокидывать через юзкейс в репозиторий, но кажется этот вариант нарушает созданные нами же ограничения о том, что юзкейс не должен ничего знать о технических деталях, плюс на уровне хэндлера надо где-то хранить указатель на коннект к базе, а из репозитория убирать указатель на базу, так как теперь он будет приходить из вне. То есть мы опять придём к тому, что перемешаем мух с котлетами

Это классический с++ на го)

Но в го для транзакций надо сагу или оркестратор а не вызывать из хандлера чужие методы

Не вижу асинхронности. Все приколочено гвоздями. Тут спутаны менеджер обьектов и управляющая им логика

Операция new дорогая. Дешевле прокидывать сам json

Сейчас расстаюсь командой, которая пишет проект по похожей структуре модулей. Вы еще забыли написать, что дофига чего (транзакции) передается через контекст. Доступ в таком бардаке невозможно определить - потому чужеродный rbac в виде мидлвари сервера, а изнутри нет сведений об авторизации. Как вишенка - каждый файл это структура с методами - и все зависимости через uber-fx... Читать это - такое себе! Сначала ждет истерика - я хотел вопить в голос и бить мебель или забиться в угол и плакать.
1. Важно, что такая структура делается, когда неинтересно изучать бизнес. А имхо, программисты описывают бизнес в коде, и если не изучают, то на выходе - говно.
2. Ужасная организация CRUD, потому что типа REST. На моем проекте нагенерировали говнокрудов, где на входе куча лишних данных - и они тупо передаются в базу. Когда в реале один пользователь может только одно поле, другой два других поля - и это красиво сделать через два разных запроса, чем жуть через REST.
3. "Обработка ошибка бизнес-логики в обработчике" - холиварная тема. У меня перед глазами пример, когда люди не поняли где запрос, а где бизнес - и от слоя контроллеров осталось одно название. Я совсем недавно познал дзен и теперь для меня wr-обработчик является бизнес-логикой. Охренеть, как красиво в результате получается.
4. Менять бд - а сколько раз видели такое? Логика очень сильно привязана к хранилищу. И даже в моем проекте ребята как ни пыжились, то все равно у них отдельно постргя и отдельно эластик - и абстракция жирно поверх - грим на язвы!
5. Из-за того, что лень изучать бизнес, то куча неявных вызовов и зависимостей. Названо это "модульная архитектура, можем удалять целыми модулями - и красота!". В реале никто модули удалять не будет - удалишь один и теперь поддерживать ДВА продукта!
6. И почему-то не используется изоляция через internal. Да, через интерфейсы есть изоляция. Но это показывает то, что разраб не знает что ему нужно от модуля.
7. Тестить это не реально. Юзаются http-тесты и всё!

  • Передавать все в контексте - антипаттерн, в контексте лежит по минимуму и реально необходимое. В моей структуре как раз наоборот практически ничего в контексте не лежит, а передается от слоя в слой через аргументы

  • CRUD - не панацея. Организуйте как удобно, тут про слои и их взаимодействие

  • 3 пункт звучит круто, никогда о таком не задумывался, спасибо)

  • БД - абстрактный пример. Лично я за свою жизнь пережил переезд на другой логгер и на другую версию тарантула, без интерфейсов - больно

  • Как раз тестируется это проще за счет интерфейсов "с двух сторон", мокаешь и делаешь юниты, не вижу проблемы в тестировании.

Спасибо за виденье, учту в следующих своих проектах)

Пардонюсь, какие-то слова относились к тому проекту, который видел.
Обобщу - какую-то структуру делают все. Как говорится "любой план лучше никакого". А названия модулей и разделение по слоям - где остановиться?! - холиварная тема.
Точно антипаттерн - писать универсальные слои абстракции. Это примерно как дженерики в гошке - они решают тему копипасты, но точно не бизнеса.
И то же самое, но в позитивной формулировке - хороший код хорошо описывает бизнес... оппа вижу тут интересную тему, что я пишу про бизнес-код (наверно, потому что про сущности), а интерфейсные вещи типа http-сервера пусть будут абстрактными. То есть мой тезис - бизнес-код должен быть максимально конкретным, чем точнее тем лучше описывает бизнес.

Сам по себе принцип инверсии зависимости хоть и дает возможность написать какие никакие тесты, на практике этого недостаточно. Или тесты становятся дорогими в написании и поддержке (и даже mockery не в помощь) или бесполезными. В оригинальной книге тема тестирования пронизывает каждую главу, вся чистая архитектура буквально про то, как делать тестируемые приложения. Здесь же об этом пару слов.

Возможно, для микросервиса вопрос не очень актуальный, но как вы боретесь с нарастанием количества сущностей? Я сейчас работаю с системой, там есть 216 сущностей, поддерживающих операции CRUD. Создавать вот такой бойлерплейт для большого числа сущностей... выглядит оверехдом имхо.

А какое решение используется у вас в проекте, если не секрет?) 216 сущностей в одном сервисе, мне кажется, в любом формате и архитектуре тяжело поддерживать

Сразу оговорюсь, что речь не про Go.

У нас описания сущностей (по сути правила маппинга на модель базы) сгруппированы согласно бизнес-логике. А вот отдельных репозиториев изначально нет вообще, и методы CRUD реализованы в абстрактном репозитории. И только если сущность имеет какой-то уникальный CRUD, то для неё реализуется свой, отдельный репозиторий.

Таким образом в 95% случаев нам надо поддерживать только саму сущность, без обвязки.

Звучит как питонячия история)

На самом деле мне не кажется поддержка большого количества сущностей проблемой, тебе нужен юзер, ты проходишь по его роутеру, юзкейсам, репозиторию, зачем тебе думать о других сущностях)

Еще в больших проектах иногда выносят стораджи в отдельные сервисы и за взаимодействие с БД отвечает другая команда. Возможно, иногда, как вариант решения проблемы)

Нет, не питон.

мне не кажется поддержка большого количества сущностей проблемой, тебе нужен юзер, ты проходишь по его роутеру, юзкейсам, репозиторию, зачем тебе думать о других сущностях

Это разве не будет в 95% случаев бойлерплейтный код?

216 сущностей в одном микросервисе имхо повод не называть его микро разбить на более мелкие.

Имхо, разбивать сервис на несколько только из-за количества сущностей - глупо. Микросервисы, ради микросервисов...

Всё это очень похоже на изобретение велосипеда, который давно "изобретен" в других ЯП и фреймворках, например в PHP в symfony/laravel/yii и т.п. Считаю, преимущество голанга в горутинах (эффективная многопоточность), быстродействии и надёжности (компиляция), а также во всяких элементах 12 factor app, например общение с децентрализованными бекендами бинарными протоколами и т.д. Зачем пытаться сделать из голанга язык для сложной бизнес логики и веба (который смотрит на клиента-браузера) - хз...

Для чего по вашему гошка, если не для сложной логики и бекенда?) Замысловатая прокся в Джаву?) И чем должен обладать язык для "сложной" бизнес-логики? В чем вообще заключается сложность бизнес-логики и как выбор языка на нее влияет?

Простая, быстрая, лёгкая ещё и с горутинами (Как раз огромный плюс для веба, когда бекенд на джаве отвалится по воркерам, гошка будет спокойно работать на горутинах), имхо странный тейк про то что голанг не для веба, если даже гугл его изначально под это и пилили. Да и по опыту всего бигтеха гошка себя отлично показывает

Хорошо написано 👍🤘💪Но тему можно развить, началом процесса может служить как внешний, так и внутренний вызов, rest-api, gRPC-api, event-driven, выполнение по расписанию итд. В различных случаях может быть различное поведение аутентификации и авторизации, в каких то сценариях может вообще отсутствовать. Могут быть различные сценарии в каком виде возвращать тот или иной domain object, т.е. какие то поля удалять или маскировать в зависимости от авторизации. Да и в целом с авторизацией много чего связано какие объекты можно получать (фильтр для выборки), какие действия и над какими полями объекта можно производить итд. Для цепочки внутримикросервисных вызовов должна быть возможность включить паттерн Unit Of Work, задать уровень изоляции итд. Из коробки/шаблона для каждого микросервиса должно заводиться Observability healthchecks (желательно проверять всю инфраструктуру используемую микросервисом), трейсинг (для всей межмикросервисной цепочки, для всех способов общения), логи сваливающиеся в одно место с ссылкой на трейсинг. Для общения между микросервисами должны быть клиенты скрывающие под капотом протокол общения, при необходимости реализующие систему ретраев, логирование, алертирование, передачу трейсинга.

Описанная архитектура слабо относится к Go имхо. Реализовать так можно в любом языке.

Когда я вижу просто классик который лежит со всеми при этом называется UserService это прям взрыв мозга , когда в архитектуре есть отдельные специальные Service

@BincomAD а можете показать пример организации файлов и папок в таком проекте?

И из статьи не совсем понятно, есть ли выделенный слой модели API? Или все JSON поля живут в Entity.

В небольших проектах я кладу прямо в entities, в больших я бы разделил, чтобы не мешать ответственность, можно положить где-нибудь рядом с хендлерами/адаптерами.

По структуре примерно это может выглядеть так:


Service/
cmd/
config/
internal/
app/
apiserver
pkg/
delivery/
router/
entities/
repository/
usecase/
pkg/
dbadapter/
middleware/

Понятно, спасибо.

Рекомендую привести нейминг пакетов с учетом Effective Go, избавиться от пакета config и убрать излишнюю вложенность пакетов (и укрупнить их).

Привет! Я долго тружусь над статьями, но некоторыми мыслями или наблюдениями хочется делиться в более быстром и доступном формате, что-то хочется обсудить или узнать у более опытных людей)

Не знаю на сколько можно делиться ссылками у себя в комментариях, но я решил, что хочу начать вести свой ТГ-канал и делиться мыслями там: https://t.me/siliconchannel. Надеюсь никого не смущает такая самореклама в комментариях к своей статье, надеюсь кому-то может быть это интересно

Респект автору за такое внимание к деталям, но мне кажется он перепутал языки)

  1. Это не совсем Go-way, а больше похоже на подход из мира Java. Суть Go – это простота и минимализм. В статье же используется подход, характерный скорее для Java и Spring Boot, где всё максимально сложно и замешано на абстракциях.

Почему это не очень хорошо?

В Go всё просто и прозрачно, а здесь автор усложняет архитектуру, добавляя множество лишних слоёв: DI, интерфейсы, UseCase и адаптеры. Всё это создаёт дополнительную нагрузку, но реальной пользы почти не несёт.

Простой пример из статьи:

updatedUser, err := userUsecase.Update(user)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

Нужно ли для простого обновления пользователя создавать отдельный слой UseCase? Возможно, было бы проще напрямую вызвать метод, который взаимодействует с базой данных.

Ещё одна проблема — это репозитории.

Автор говорит, что репозитории помогают легко менять базу данных. Но, если честно, как часто это реально нужно? На практике почти всегда используется одна конкретная база данных (например PostgreSQL или MySQL), и никто её просто так не меняет.

Из-за таких лишних слоёв абстракций код становится запутаннее и сложнее поддерживается, хотя реальной пользы обычно нет.

Как можно сделать проще?

Вместо создания сложных конструкций с интерфейсами:

updatedUser, err := userRepo.Update(user)
if err != nil {
    return err
}

Можно просто написать простой и понятный запрос без лишних обёрток и промежуточных слоёв.

Следующий момент — это огромный файл main.go.

В статье автор создаёт «мега-роутер», который содержит в себе практически всё: настройки middleware, роуты, DI и так далее.

Почему это неудобно?

Обычно в Go принято, чтобы файл main.go отвечал только за запуск и инициализацию приложения. Весь остальной код лучше распределять по разным пакетам и файлам, чтобы было легко читать, понимать и поддерживать.

Как лучше?

Держать main.go простым и понятным, а роуты и обработчики запросов хранить отдельно, чтобы каждый файл отвечал за что-то одно.

И последнее — Dependency Injection (DI).

Автор утверждает, что DI нужен, чтобы скрывать реализации и легко заменять зависимости. Но если задуматься, в Go это редко оправдано. DI активно используется в Java, потому что там большие фреймворки и много магии. В Go же все намного проще, и DI обычно приносит больше головной боли, чем пользы.

В большинстве случаев код на Go можно сделать простым, понятным и читаемым и без этих слоёв и абстракций.

Что в итоге?

Статья в целом крутая и интересная. Мне понравилось ее читать, но автор усложняет то, что на самом деле должно быть простым. Если делать сервисы на Go, лучше следовать его философии простоты и минимализма. Тогда код будет понятным, а приложение — удобным в поддержке и развитии. Всеx обнял. 😄

Sign up to leave a comment.

Articles