Как стать автором
Обновить

Комментарии 35

Мне кажется, вы зря в Go пытаетесь притащить подходы из Java.

Go - это практически C. А у вас тут ORM, DDD, 100 слоёв абстракций.

В чём проблема затащить в go DDD и легковесную ORM? Как будто язык запрещает вам определить собственные доменные модели и наикнуть в них методы для бизнес-логики. А большинство ORM в го довольно легковесные и по своей сути скорее напоминают библиотеки для исполнения sql-запросов

Правильно ли я вас понял, что вы считаете что подходы чистой архитектуры не применимы к С и Go?

Правильно ли я вас понял, что вы считаете что подходы чистой архитектуры применимы только при Java-like программировании?

Ответил вопросом на вопрос потому-что ваш вопрос показался крайне токсичным и не конструктивным. Но правильно будет ответить по другому:

DDD и чистая архитектура по Мартину в своей сути предполагает чистые функции и разделение слоёв что вполне реализуется на любом C подобном языке. Автор комментария намекает на то что Go не Java и не надо в Go тянуть реализации подходов из Java. 100 слоёв абстракции и развесистые ORM это не Go-way. Для Go нужны свои решения, простые и лёгкие, более подходящие под философию языка

но ведь на Go тоже пишут сложную логику

Имхо, ниша Go - это скорее небольшие и средние программы с упором на эффективность (обычно микросервисы или не слишком развесистые консольные утилиты). На Go редко пишут монолиты.

Упор на эффективность при относительной простоте написания кода.

То, что описано в статье - упор на чистоту кода, гибкость, переиспользуемость и т.д., т.е. немного в другую сторону. Грубо говоря, ORM хорош для чистоты кода, но ужасен для производительности (query builder еще ок, но не ORM). 100 слоёв абстракции с выделением чистейшего слоя бизнес логики хорошо для монолита, но для микросервиса с тремя таблицами - вообще ни к чему, эффективнее часть логики держать в SQL

В целом, считаю, что если надо писать код, который прям требует DDD, ORM и проч, лучше писать его на Java / PHP / C# ...

У вас будут большие проблемы с сопровождением проектов.
Мы пишем десятки микросервисов на Go, и все они поделены на слои. Это всегда облегчает покрытие кода тестами, и даёт все плюсы чистой архитектуры.
Отказываться от этого, но ради чего?

Правда я лично не совсем приветствую вводить какой либо ORM в Go, если можно легко обойтись без него.

эффективнее часть логики держать в SQL

Считаю что не стоит вводить какую либо логику на стороне БД.
Как вы будете покрывать эту логику тестами? Как вносить изменения? Что будет если поменяется тип базы данных, например на NoSQL?

БД это один из инструментов. Не стоит связывать инструменты с бизнес логикой.

Рекомендую ознакомиться с книгой "Чистая архитектура" Роберта Мартина.

Я в курсе про чистую архитектуру.

В микросервисах на Go какие-то слои нужны, конечно, тоже, но не настолько. Слой юзкейсов от слоя хранения отделять слишком дорого.

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

SQL - это уже своего рода абстракция над бизнес-сущностями. Там уже описаны связи. Используя ORM вы дублируете эти описания, при этом существенно жертвуя производительностью, потому что начинается магия по подкапотному генерированию SQL через фиг пойми какую магию.

> Что будет если поменяется тип базы данных, например на NoSQL?
А что, если не поменяется? Вероятность смены pg на mongo в микросервисе около нуля (на моей практике ровно 0), а вот усилия по поддержке независимости от бд очень существенные, которые надо платить каждый день. Т.е., в среднем просто не отобъётся. Недавно писал про это как раз: https://t.me/crossjoin/243

Слой юзкейсов от слоя хранения отделять слишком дорого.

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

Используя ORM вы дублируете эти описания, при этом существенно жертвуя производительностью, потому что начинается магия по подкапотному генерированию SQL через фиг пойми какую магию.

Давайте рассматривать проблему без ORM. Это отдельная тема. Представьте что у вас под капотом выполняются голые SQL запросы.

А что, если не поменяется? Вероятность смены pg на mongo в микросервисе около нуля (на моей практике ровно 0), а вот усилия по поддержке независимости от бд очень существенные, которые надо платить каждый день.

У нас был опыт перехода на mongodb просто потому что с ней было удобней работать. И в перспективе мы получаем важные для нас плюшки.

Реляционная модель БД изначально появилась из-за того что она отражала принятую в бизнесе работу с табличными данными. Поэтому кажется что вполне нормально держать высокую связанность бизнес логики с базой. КМК это заблуждение.

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

Давайте рассматривать проблему без ORM. Это отдельная тема. Представьте что у вас под капотом выполняются голые SQL запросы.

Структуры окей, но не в структурах же дело. Например, надо выбрать посты для отображения топа постов, но так чтобы в топе не было двух постов от одного юзера. На SQL это делается, например, как group by user_id и having count(*) = 1

но это условие having count(*) = 1 - это бизнес-логика в SQL запросе. Которую надо, например, тестировать.

И как мы тут дешево абстрагируемся от хранилища? И так, чтобы тесты еще и реально что-то тестировали.

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

У меня мало опыта с такими кейсами, но я бы попробовал зайти через опции, например так:

func main() {
 _,_ = repo.Posts(repo.WithOneUser())
}

// В репозитории
func WithOneUser() Option {
  func(r *Repo) {
    r.havingCount = 1
 }
}

Либо отдельный метод:

func main() {
 _,_ = repo.TopPostsOneUsers()
}

Вопросы к неймингу больше.

как вы протестируете, что ваше repo собирает правильный запрос для базы, и всё работает?

а если надо еще приджойнить что-то? Отобрать посты, где статистика (которая в другой таблице) топовая?

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

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

как вы протестируете, что ваше repo собирает правильный запрос для базы, и всё работает?

Буду тестировать через unit тесты дёргая публичные методы репы. Так как мы следуем правилу инверсии зависимостей, то у нас будет скорее всего в зависимостях некий интерфейс клиента БД и через мок сможем проверить собранный SQL запрос.

Либо зайти через интеграционный тест и поднять уже реальную базу.

Самое главное что бизнес логика останется в стороне и ни как не будет знать про БД. Тем самым мы не нарушаем принцип единственной ответственности.

Тесты пишутся часто с использованием базы.

У нас тоже есть тесты с базой данных, но это исключительно интеграционные тесты.
Бизнес логику мы покрываем с помощью unit тестирования. Это намного раз легче и удобней, в том числе в сопровождении (был опыт в покрытии тестами высокосвязанного кода).

100 слоёв абстракции с выделением чистейшего слоя бизнес логики хорошо для монолита, но для микросервиса с тремя таблицами - вообще ни к чему

Ну вот вам ситуация из жизни. Есть топик кафки, который читают 15 микросервисов. Раньше там в топике был json, а потом после смены контракта там оказался protobuf. Без чистой архитектуры вам пришлось бы копаться в бизнес-логике этих сервисов. А так можно просто поправить самый верхний слой, который конвертит внешние данные в доменные модели, и спокойно жить дальше.

В целом, считаю, что если надо писать код, который прям требует DDD, ORM и проч, лучше писать его на Java / PHP / C#

ORM - спорная штука, которая даже в C# мне не нравилась. Но что не так с DDD в go?

Ну тут смотря какие слои. Отделять условные контроллеры от сервисов можно, конечно, и это очень дёшево. Почему бы и нет.

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

Отделяют обычно ради тестируемости. Интеграционное тестирование в масштабах большой кодовой базы становится очень дорогим.

Даже если у вас БД доступна из любого места в приложении вам придется решать как приводить агрегат в согласованное состояние и не забыть нужные поля изменить.

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

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

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

Честно говоря, вообще непонятно как вы умудряетесь работать с бизнес-логикой на таком очень низком уровне абстракции и без нормального ООП. По идее вы должны в слой БЛ слать только JSON, например, с сущностью. Там json конвертируется в структуру данных пригодную для алгоритмов, которую по некоторым подпискам можно поменять скриптами адаптированными под бизнес-процессы конкретные и после целиком это запишется в БД, Бог знает в каком виде, лучше никому и не знать. Т.е. вы записываете заказ. Записывает оплату. А когда запрашиваете у БЛ заказ, она сама соединяет данные и дает статус заказа. Т.е. его даже и не надо через сущность трогать, это свойство ридонли для заказа. Связанность то тоже надо контролировать. В этом плане event-sourcing прекрасно поможет.

"При росте сложности бизнес-логики" ... покупается 1С и все добро туда сваливается, потому что сочинять свой слой с БЛ очень дорого. Заказ - оплата - резерв - отгрузка - доставка - получение. Это уже необходимый минимум. А если еще и сразу резерв, до оплаты, а вы заказ меняете мимо логики.

В чем плюсы пересылать JSON в слой БД?

Там написано "БЛ", Л вторая. Вы хотите Model-View-Controller собрать на процедурном языке? Очень дорого и результат не гарантирован.

В 1с например просто присваивают значения полю, как указано в разделе "В начале было Active record" и проблем нет. Есть правда события, например "ПередЗаписью" где мы можем проверить и дозаполнить необходимые поля. Я думаю в Go по аналогии нужно не в метод Apply передавать разные функции, изменяющие объект, а сделать метод Save(ctx) и в нем закодировать логику расчета доп полей перед помещением в БД.

Да, это отличный вариант

Например, это позволяют делать обработчики событий BeforeSave ORM gorm и ent. Но такое поведение является непривычным для большинства разработчиков и требует отдельного упоминания на онбординге.

сделать метод Save(ctx) и в нем закодировать логику расчета доп полей

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

Так не запрещено же из одного метода вызывать другие процедуры и функции.

Тут есть 2 проблемы. Если довести до предела и вызывать одну функцию, которая все нам посчитает (и будет вызывать другие, если надо), то к чему вся эта канитель?

В чем принципиальная разница между этим

Save(ctx)
  {
  order.price = MegaFunction(ctx);
  }

и этим?

order.price = MegaFunction(ctx);

Есть другая проблема, когда логика расползается между методом Save(ctx) и всякими хелперами, утилсами и прочим.

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

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

Тут нет никакой реальной проблемы, равно как и нет необходимости в обязательных геттерах/сеттерах для всех полей всех объектов. Это просто разные задачи, которые можно решать независимо друг от друга:

  • Мапперы/ДТО позволяют внедрить чистую архитектуру, т.е. отвязать бизнес-логику от внешнего мира и сделать её легко тестируемой юнит-тестами.

  • При необходимости контроля мутаций сложных объектов бизнес-логика, получив на вход ДТО и отвалидировав его, может решать эту проблему как ей удобно. Напр. преобразовав входной ДТО в тип с (частично или полностью) не экспортируемыми полями и методом для экспорта обратно в ДТО плюс методами для необходимых мутаций либо используя вышеупомянутый линтер для защиты полей ДТО либо какими-то другими соглашениями с контролем их на ревью/кастомным линтером.

Так и знал что будет холивар. ))

Пожалуйста, покажите рыбу проекта. Я, пока не увижу код в репке, ничего не понимаю.

Постараюсь сделать, но увы недельки через 2 только.

err := orders.Apply(ctx, orders.FilterByID(orderId), func(order orders.Order) error {
	_ = order.RemoveGoods(goodFilter)
	_ = order.AppliedAbsolutDiscount(0.8)
	_ = order.AddGifts(user)
    // Опустил обработки ошибок
	return nil
})

Я так пишу, очень удобно, orders.Order - интерфейс состояния агрегата, прямого доступа к свойствам нет, менять структуру состояния можно в любой момент и как угодно. Методы кидают ошибки бизнес логики, если не могут выполниться.
Apply вторым аргументом может принимать интерфейс фильтра, который внутри orders может превращаться во что угодно. Все это живет с gorm очень хорошо. Количество фильтров строго ограничено, они все исключительно бизнесовые кроме FilterByID.

Репозитория как такового нет. orders по факту инкапсулирует состояние и его хранение, принимает в себя коннект к бд, в моем случае gorm.

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

Методы AppliedAbsolutDiscount создают события внутри состояния, которые в конечном итоге сохраняются в бд и обрабатываются аутбоксом.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории