Обновить
4
0

Пользователь

Отправить сообщение

Просто перед тем как делать, надо думать.

Делайте run асинхронным. И не надо ничего маскировать. Нет ни одного случая, когда проблема описанная в начале статьи будет актуальной. Если верхнеуровневый по Dependency Inversion модуль требует синхронный интерфейс, чтобы дернуть низкоуровневый модуль, значит, сразу вызываем асинхронную инициализацию модуля низкого уровня, а потом уже дергаем верхнеуровневый модуль (прямой контроль). Либо через IoC (инвертированный контроль) по лайфсайклу все инициализируем.

По поводу обработки ошибок - обычного try/catch вместе с async/await будет достаточно в обоих случаях, если код просто синхронный и код асинхронный. Даже извращение в виде T | Promise<T> должно нормально поддерживаться, не проверял, но по идее обернуть в один try/catch обработку результата такой функции будет достаточно, если не забыть дернуть await для кейса Promise<T>.

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

Вы уже какой-то сублимацией занимаетесь: "Не авторизация, а аудит", "контексты для контекстов".

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

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

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

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

Поэтому в данном контексте с ссылкой все то же самое - создатель ссылки сделал ключ. Рассказал 10-ти друзьям, где он лежит. Кто конкретно использует этот ключ ему не важно, можно назвать это отсутствием персональной идентификации, но созданием идентификации группы персон. Может быть и так, что кто-то не посвящённый нашел этот ключ случайно - он не авторизован в глобальном контексте, но в локальном авторизован. (Если я нашел чей-то активный API ключ - система меня пропустит, но очевидно это нарушает правила сервиса).

Так что все очень относительно. Я в принципе описал систему, которая только делает авторизацию (обычная дверь и обычный ключ). Может быть дверь с замком с биометрией, которая сразу проведет идентификацию, а уже потом авторизацию.

Владелец аутентифицирован, я анонимный пользователь, с которым поделились ссылкой, не аутентифицирован, но авторизован.

Задача была авторизовать анонимных пользователей. Задача выполнена.

Если вы не это имели в виду, то приведите прозрачные тест кейсы.

Надо смотреть. В общем случае число if/else кейсов будет одинаковым в сравнении с подходом написать в лоб в процедурном стиле, просто скрыто за полиморфизмом или слабой связанностью. Но именно удобство чтения и расширения будет больше, при условии, что получается смоделировать какие-то ключевые узлы механик.

Я всегда в таком случае просто пишу 2 версии и сравниваю, чтобы понять, что выглядит более удачно. Занимает больше времени на старте, но даёт возможность сравнить и принять решение. Пока что я чаще всего выбирал ООП подход, но где-то можно и процедурами сделать, если механика непонятная и весь этот оверхэд на скелет может не окупится.

Но я не разработчик игр, я могу очень многое упускать из виду.

Не вижу проблемы. AttackInteraction должен все это учесть. Он должен пройтись по текущим эффектам на цели и обнаружить, что на ней весит эффект левитации. Далее через слабое связывание (например, рефлексию) он должен посмотреть, какие специальные правила работают для левитирующих объектов во время атаки (например все атаки ближнего боя гарантированно промазываются). Далее с помощью рефлексии создаётся объект этого правила и ему прикидывается контекст взаимодействия. Првило рассчитывает шанс промаха 100%. Но он так же проверяет, есть ли среди эффектов атакующего перк IgnoreLevitationForMeleePerk. Если нет - ставим шанс промаха 100%, если да то 0% условно. И так с базовой атакой и так же с другими модификаторами. Т.е. каждое взаимодействие является скетелом механики и позволяет подключать к ней любые частные случаи за счёт Dependency Inversion и Inversion of Control.

Интерфейс может выглядеть так: IDefenceEffectHandler<LevitationEffect> { HandleEffect(LevitationEffect effect, IAttacker, IAttackTarget).

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

Соответственно интерфейсов будет много. Одни будут считать общие правила по эффектам. Другие будут описывать правила как тролли взаимодействуют с призраками и ТД (другая плоскость/матрица расчета) IAttackerAndTargetSpecialRule<Troll, Ghost>. Далее надо будет определить, что считается в первую очередь - общие правила по эффектам или специальные правила.

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

"Критикуешь - предлагай".

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

По сути 3-тий класс это некий Interaction между объектами: AttackInteraction, MagicInteraction, DialogInteraction, BarterInteraction(Inventory buyerIntentory, Inventory sellerInventory, IBarterActor buyer, IBarterActor seller), LootInteraction, StealInteraction и тд. Этих взаимодействий должно быть ровно столько, сколько их есть в игре. Вычленяются они из естественного языка: Игрок атакует Моба; Моб атакует Игрока; Игрок лутает Моба; Игрок берет квест, Игрок и тд.

Т.е. есть изолированные Components: Статы, Инвентарь, Дерево Умений, Изученные заклинания, Перки, Ячейки заклинаний и тд. Например при нажатии кнопки Loot у убитого врага, открывается UI вашего инвентаря и убитого моба. При попытке забрать айтем запустится new LootInteraction(Inventory source, InventoryItem item, Inventory target).Execute()/CanExecute(), которая вызовет target.CanPutItem(item) и если true, уберет айтем из source инвентаря и положит в target инвентарь.

А такие классы как у вас в примере: AuraDamageProcessor являются "чистой выдумкой" из GRASP и по большому счету нужны просто для декомпозиции кода. Жить такие классы всегда будут где-то в той же папке, где описывается Interaction или Component.

Но не согласен, что это становится неподдерживаемым, если в проекте 200+ таких классов: сложность понимания флоу и механик для разработчика должена быть сопоставима с сложностью понимания флоу и механик для игрока. Если получилось 200+ компонентов и интеракций, ну значит игра не простая в плане механик, легкой поддержка точно не будет простой при любых раскладах.

Зато все классно тестируется. Можно просто запустить целую игровку сессию, задать стартовые состояния объектов кодом, и в рамках юнит теста, вызывать методы компонентов и взаимодействия между ними, симулируя поведение игрока и мира, и делать snapshot мира (save game) после каждого действия, и сравнивать его с ожидаемым значением. И можно запилить интеграционный тест, и то же самое провернуть с игровой сессией уже в рамках интеграции с движком.

По-другому это как? Юнит может иметь разные навыки в зависимости от того, в отряде он или нет? Он обладает всеми навыками сразу. А вот какие будут использоваться зависит от стратегии - одиночка, малый отряд, батальон.

За этими микросервисами в 99.9% случаев будет стоять stateful база данных. Поэтому да, это интерфейс доступа к базе, препроцессинга/пост процессинга данных, маппинга, интеграций, аггрегаций и ТД. И масштабируется такие микросервисы очень легко автоматически по различным метрикам: CPU/RAM/среднее время респонса за последние 5 минут.

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

Поэтому масштабирование микросервисов без масштабирования базы данных зачастую бессмысленно.

Кроме остальных 0.01% микросервисов предназначенных для stateless процессинга in/out вроде обработки фото, видео, нейронок где нужно много CPU/RAM/GPU и есть смысл выделить этим функциям отдельный compute. И там соответственно масштабирование тоже относительно простое и автоматическое по метрикам потребления ресурсов.

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

Все по факту сказано, согласен.

если вероятность конкурентных обновлений существенна - разрывайте транзакцию

Вы уже на ходу свою позицию адаптируете, в вашей статье про это ни слова, более того выше вы отстаивали, что данное решение нерабочее.

ошиблись? исправляйте ошибку в application layer, доменный слой при этом останется незатронут

Я вам уже объяснял, что дешёвым это изменение может не быть. Потребуется изменить и UI/UX, и протащить это по системе. Так что правило надо инвертировать: сразу смотрим что за ценность у модуля, какие риски, и если их мало - делаем в одной транзакции, но как правило в demanding модулях делаем асинхронный флоу.

Ладно, я устал спорить, в данном случае это путь в никуда.

Я привожу вам примеры и кейсы, когда ваше решение и виденье не работает, с явками и паролями. А вы просто обвиняете, называете мое понимание "неправильным", без примеров, по сути голословно.

Читатель комментариев пускай сам решает, кто на его взгляд прав и проверяет на практике.

Что-то я не очень понимаю, в чем мое решение нерабочее?

Мое решение как раз рабочее безотказно, но к сожалению с бОльшим оверхэдом на проектирование. Т.е. придется сразу перевести аккаунт в состояние "Архивируется". Это предотвратит дальнейшее его изменение в процессах не связанных с архивацией. Далее на интеграционное событие я отреагирую установкой триггера через пару секунд. Это позволит завершится всем операциям записи голосовой почты. Далее я точно буду знать, что новых записей голосовой почты в этот аккаунт поступать не будет, т.к. юз кейс на добавление записей проверяет статус активации аккаунта. Все, далее по вышеописанному триггеру можно безопасно пагинировать записи, зная, что никаких конфликтов не будет. Когда процесс архивации каждой записи отдельно закончится - я отправлю команду в аккаунт на изменение статуса с "Архивируется" на "В архиве". Решение масштабируемое, независимое от технологий SQL/no-SQL, с четкими границами консистентности, без подводных камней.

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

Nothing except of scalability and consistency of course.

Я же вас не предлагаю не использовать это, я говорю использовать это осознанно и с большими оговорками, а не "ваше понимание странное".

Ниже я даже писал, что сам так иногда делаю, в супер простых кейсах. Ровно как и @michael_v89.

Ваше странное понимание против нашего странного понимания

А в чем смысл я объяснил. Хотите максимально сильную защиту всех инвариантов в системе, проектируйте систему через один агрегат (например, Банк). Но перфомансу и масштабируемости придется сказать пока.

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

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

Агрегаты защищают инварианты, а не спасают от конкурентных обновлений :)

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

Ну да, я же, убогий, за 20+ лет практики реальных-то приложений не видел...

Я вас таким не называл. Мысленный эксперимент с различными базами в различных BC и разными требованиями с масштабируемости должен навести вас на мысль, что не все так просто. Ваш доменный слой по идее отличается не будет никак, если вы используете синхронные ивенты или обновляете несколько агрегатов в одной транзакции. (Т.к. у вас репозиторий в домене, то будет меняться репозиторий). А вот апликейшен слой будет отличаться. Особенно, когда меняются требования к инфраструктуре. И вот этот момент надо понимать. Переписать с синхронного флоу на асинхронный требует не только изменить контекст выполнения кода, а вообще переосмыслить весь бизнес процесс, пользовательский опыт и ТД.

А причём тут вообще DDD? А он тут совершенно и непричём. Вообще никакого отношения к вопросу не имеет.

Имеет ещё как. DDD это не только про доменный слой. Сам по себе без оркестрации апликейшен слоем собрать какой-то мало-мальски полезные процесс не получится. Это и есть User Story. И когда вам надо менять стори, потому что поменялась инфраструктура - это текущая абстракция от инфраструктуры.

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

Стремиться надо к такому проектированию, когда инфраструктура на домен и апликейшен влияет минимально. Тогда будет расти гибкость.

Нет, никакие границы тут, разумеется, не нарушаются, да и с чего бы вдруг?

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

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

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

  • Ваша система масштабируется, юзер хранится на одном инстансе одной базы, а история в другом; или например юзер в MySQL а история в elastic search. В вашем подходе абстракция течет, говоря о том, что строго настрого для выполнения операции архивации пользователя он сам и его история должны принадлежать к одному UoW. Т.е. не получится партиционировать историю звонков по годам. Строго говоря, в домене такого ограничения нет. Когда появится необходимость оптимизировать хранилище (инфраструктуру), записи за 2025 год хранить на теплом инстансе, а за 2023 в холодном сторадже, придется объяснить бизнесу, что для этого придется переписать кусок логики.

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

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

Да, разумеется, и это главная фича доменных событий.

Жизнь заставит вас пересмотреть свои взгляды, когда наиграетесь в DDD на локалхосте и начнёте масштабироваться. Когда окажется, что одни данные в MySql, другие в elastic, mongo и тд, синхронные макеты вам не помогут. А если начнёте с одной базы и синхронных макетов, готовьтесь переписать под приложения, чтобы внедрить поддержку асинхронности на должном уровне.

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

В этом месте корабль DDD затонул из-за дырявой абстракции. В этом месте вы явно объявляете, что раз модуль А ловит событие модуля Б синхронно, то одно из двух: либо есть риски неконсистетного состояния, либо ваш UoW обслуживает оба модуля. Во втором случае это текущая абстракция. Модуль А независимым от модуля Б, но по факту неявно они делят контекст и без одного не может быть другого. И когда у вас произойдет смена хранилища из-за разных требований к масштабированию модулей (деманд модуля Б растет, а модуля А нет) - придется лопатить весь апликейшен код.

Текущая абстракция = при изменении инфраструктуры, неизбежно изменение application code.

Так что если вы добавите в свое представление о модулях (BC) требование, что у каждого BC может быть свое хранилище и разные требования к масштабируемости, вы поймёте, что никакой функции для получения данных в репозитории, кроме GetById быть не должно, иначе абстракция течет, делая техническую реализацию явной.

Я окей с тем, чтобы использовать синхронные ивенты, давать абстракциям мягко и иногда даже жёстко подтекать, если человек отдает себе отчёт, что он делает: вот эти модули масштабироваться не будут (какие-то разовые конфигурации), а эти будут: но в принципе они простые и если что быстро перепишем; а вот это быстро не перепишем и тд.

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

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

Получается задача выполнена, мы можем авторизовать пользователя, не аутентифицируя его в системе. (Шаг аутентификации может быть выполнен вне системы, например друг у вас попросил доступ через ТГ, или не выполнен вовсе). Системе это безразлично.

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

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность