Пример упрощен, чтобы очистить его от лишних деталей и сосредоточиться на самом паттерне. Реальность гораздо страшнее в этой задаче.) Да, вы сами решаете, что вам применять и когда, как инженер, ответственный за задачу.)
Коротко: на все ваши события подписан обработчик, который сохраняет их в базу. В Application слое вызов бизнес-логики, бросающей события, обернут в одну транзакцию с вызовом метода save() репозитория. Так вы гарантируете атомарность выполнения действия и выброса события. Отдельный класс `Message Relay` вытаскивает сохраненные события из базы и дальше оборачивает их в сообщение и отправляет асинхронный транспорт (тот же раббит). Уже консьюмер сообщения рабита вытаскивает из него событие и асинхронно обрабатывает его.
Эта статья - первая часть большой статьи про Messenger, которую я решил поделить. Во второй части я подробно на примере покажу, как работает транзакционный обмен сообщениями.
Спасибо за Ваш комментарий.) Я потому и поставил уровень статьи "легкий".) Если человек понимает разницу, то ему статья с объяснением этой разницы будет бесполезна.) Но беда в том, что я встречал очень многих разработчиков, которые этой разницы не понимают, и это воплощалось в их коде.
Возможно, я слишком большой акцент сделал на различии понятий и слишком малый - на архитектуре. Идея-то была показать, где, в каком слое, в проекте с хорошей архитектурой место события, где - его обработчиков, а где - транспорта.
Основная мысль, которая, возможно, утонула в лингвистически разборах, о которых Вы говорите, состояла в том, что доменное событие, прежде чем дойти до внешнего консьюмера, проходит длинный путь преобразования: Доменное событие -> Domain (!) Event Dispatcher -> Обработчик -> Message (перзистентное) -> Relay -> сериалайзер Symfony Messenger -> Транспорт Symfony Messenger -> Rabbit MQ и далее в консьюмере обратный путь через десереализацию к хандлеру, который уже совсем не доменный.
Это, на самом деле, первая часть большой статьи, которую я решил разделить. Спасибо, что подсветили, во второй части сделаю прям больший упор на архитектуру, код и путь движения события через слои.
Да, сам факт необходимости в таких людях, конечно не оспаривается.) Я это писал к тому, что когда проектируешь какой-то кусок, постоянно приходится думать: вот придут ребята, начнут его дописывать по-своему, использовать по-своему, а кто-то не увидит и сделает похожее.
Эти вопросы всегда висят в воздухе и постоянно приходится думать, как реализовать удобное для переиспользования и сопровождения решение. Желательно так, чтобы вообще не пришлось объяснять, что это и зачем. И уж тем более, чтобы не следить потом зорким глазом на ревью, ударяя по рукам всякому, кто отклонился от твоего замысла.)
Да, согласен с вами. Единственно, про CQRS не понял, причем он здесь. В статье слово агрегат применялось не в смысле доменного агрегата, а скорее как агрегируемое из разных источников представление кампании.
Спасибо за ваш отзыв! Метод supports будет первым вызван в самом "внешнем" трансформере-декораторе. Чтобы это представить наглядно, вы можете обратиться к последней схеме в статье: там где цепочка трансформеров. Если дорисовывать эту схему декораторами, то мы могли бы какой-то один из эллипсов на схеме обвести еще одним эллипсом. Вот этот второй, внешний эллипс - и есть трансформер-декоратор.
Соответственно, именно самый внешний трансформер примет запросы на выполнение операций supports() и transform(). Поскольку, при написании каждого класса трансформера, мы не знаем, как именно он будет использован в клиентском коде, то есть, будет он единственным трансформером, или в составе цепочки, будет он снаружи или внутри "многослойного" звена, нам следует реализовать методы supports() и transform() в каждом классе.
Вдобавок, своей реализацией метода supports() трансформер-декоратор дополнительно подчеркивает, что не может обходиться без nested трансформера и служит лишь для добавления каких-то дополнительных возможностей, а не в качестве самостоятельного компонента.
Спасибо за ваш вариант решения. Разумеется, любую задачу можно решить множеством способов, и каждый имеет право на жизнь.) Что лично меня здесь настораживает:
Разные потребители требуют разный состав данных, то есть не для каждого случая есть необходимость загружать весь агрегат. Лучше иметь возможность управлять тем, какие именно части агрегата будут загружены в зависимости от потребности потребителя.
Подход "один потребитель - один трансформер" - это как раз то, чего я стремился избежать, чтобы не дублировать код. Хотелось иметь набор переиспользуемых мини-трансформеров и из них уже набирать конечное решение для каждого потребителя.
Я в целом согласен с тем, что итоговое решение не блещет простотой. Но зато оно блещет очень большой гибкостью, что и было целью создания этого микросервиса.)
Не знаю, мне давно хотелось рассказать об этом интересном кейсе: как пошаговое решение задачи само по себе приводило к добавлению все новых паттернов.)
Применение ООП ни к месту для меня звучит как оксюморон. Что же, делать проекты в процедурном стиле и задумываться над тем, достоин ли вот этот проект применения ООП?
На счет понимания самих принципов: бьюсь в каждой команде, где случалось работать, за то, чтобы ребята начали лучше их понимать. Скажу, что это окупается и отражается на коде в повседневной работе. Не понимаю, что такое ООП ни к месту.
На счет паттернов: да, здесь можно находить кейсы применения ни к месту. Но обычно это связано с непониманием самого паттерна и того же полиморфизма. Чаще это ситуация, когда человек заявляет, что у него реализован паттерн, а на самом деле его там и в помине нет. Такого, чтоб паттерн реализован правильно, и это было ни к месту, нечасто видел.
Тут зависит от того, что вы понимаете под принадлежностью юзера к persistence layer. Если у вас на класс User завязана какая-то бизнес-логика, и вы, например, разметили его доктриновскими аннотациями, то это - тоже смешение инфраструктурного и доменного слоев: вы добавили в доменную сущность метаданные о том, как эта сущность хранится.
В остальном, в принципе, наши с вами мысли схожи: разделяем класс юзера на два и каждый знает свое место: один помещается в инфраструктурный слой, второй - в доменный. Вы говорите о других видах слоев, но суть остается прежней.
На самом деле, никакой трагедии в походе в базу по первичному ключу нет.
Да, все верно. Трагедии нет. Просто, как я писал, у каждого решения есть плюсы (профиты) и минусы (цена). Соответственно, не упомянуть об этом профите (уменьшение запросов в базу) при описании решения было бы, на мой взгляд, неправильно.)
Прям килобайты, а то и десятки
Да, есть такой минус у этого решения: к проектированию payload нужно подходить ответственно и думать о том, как сжимать токен. Способы для этого я упомянул.
А про главное собственно и не сказали: что в этом payload хранить, чтоб было достаточно принимать решение
Вспомнил мультик советский, где героя отправляли к двум дядькам из ларца.
Вы хотите получить универсальное решение, чтобы можно было его в любой проект вставлять, и больше не думать об этом никогда. Если набор таких решений когда-то появится, то поголовье нас, разработчиков, сильно сократиться.
Логика, на основе которой принимается решение о доступе, диктуется бизнесом. В одном проекте эта система ManagerAuthorities с разными видами и уровнями менеджеров. В другом - это система членств в разных группах. В третьем - система тегов, которыми овнеры(админы) размечают создаваемых ими юзеров.
В этом и состоит работа разработчика: понять требования бизнеса, спроектировать решение и реализовать его так, чтобы было комфортно с ним жить как можно дольше.
А вы пишите, что вы устали, читая статью и разочарованы тем, что ваш труд по прочтению не был вознагражден таким результатом, что вы навсегда будете избавлены от необходимости думать, на основании чего вам принимать решение.
Образуются всякие роли
Попробуйте мыслить атрибутами, а не ролями.
каждый сервис должен их держать актуальными
Обратите внимание на тот абзац, где я писал про вынос всего, что касается Security, в отдельный composer пакет.
Вообще, пока писал ответ на ваш комментарий, вспомнил о тех ребятах, которые требуют им расписывать задачу до такой степени детализации, что сделать самому эту задачу выходит в несколько раз быстрее.
Единственно, если фронт по каким-то причинам не сохранит полученный JWT, то будет ломиться со старым токеном. Так что, уйти таким способом от проверки актуальности токена, как я вначале подумал, не получится, увы.
В любом случае, Вы накинули свежего взгляда, есть над чем подумать.)
Ваши комментарии еще поосмысляю, спасибо, что пишите их. Пока пару моментов могу сказать:
Перевыпуск токена на фронте можно сделать без слежки за временем, а просто как реакцию на 401 ответ. Фронты на это спокойно смотрят.)
На счет проверки токена на необходимость отзыва раз в n минут. Это не будет работать в ситуации, которую я описал в статье: юзер вступил в группу по приглашению и сразу редиректится на страницу со списком материалов группы. При проверке токена раз в n минут юзер будет ждать эти самые n минут, получая 403 в ответ на попытки увидеть материалы группы.
То есть, здесь надо будет более тонкую логику какую-то делать. Пока еще соображаю.)
JWT, как правило, выдаются людям, а люди редко имеют статические IP-адреса. Технически-то легко сделать: добавляете IP в payload токена и проверяете при каждом запросе. Только смысл это будет иметь, если клиентами вашего приложения являются не люди, а другие приложения, работающие на статических IP-адресах.
Возможно, я просто опять Вас не так понял и мы снова говорим про разное.) Обычная схема какая: Выдается два токена: собственно JWT и refresh_token. Когда срок жизни JWT истекает, фронт (или иной клиент) автоматически запрашивает новый токен по refresh токену.
Предполагается, что если злоумышленник перехватит JWT, то он сможет пользоваться им только до истечения его срока годности, а потом, не имея refresh токена, он потеряет доступ к приложению.
При таких условиях чем меньше срок жизни JWT, тем считается безопаснее. При этом автоматический перезапрос JWT по refresh токену, осуществляемый фронтом, проходит для пользователя незаметно: его не выкидывает на страницу логина.
Прошу прощения. Я подумал, что Вы критикуете меня в форме искаженного пересказа моего текста.) Теперь, когда Вы указали, что это Вы описали свое, третье, решение, я взглянул на Ваш комментарий по-новому. Мой косяк.)
Ваше решение тоже интересно, еще подумаю над ним. Идея проверять на отзыв JWT не при каждом запросе, а раз в несколько минут интересна, тут, конечно, бизнес-требования каждого конкретного проекта будут вносить свои коррективы.
Единственно, что хотелось бы отметить, так это то, что годовалый срок жизни токена считается плохой практикой с точки зрения информационной безопасности.
Пример упрощен, чтобы очистить его от лишних деталей и сосредоточиться на самом паттерне. Реальность гораздо страшнее в этой задаче.) Да, вы сами решаете, что вам применять и когда, как инженер, ответственный за задачу.)
Очень хороший вопрос. Вам нужен паттерн "Транзакционный обмен сообщениями" Криса Ричардсона https://microservices.io/patterns/data/transactional-outbox.html.
Коротко: на все ваши события подписан обработчик, который сохраняет их в базу. В Application слое вызов бизнес-логики, бросающей события, обернут в одну транзакцию с вызовом метода save() репозитория. Так вы гарантируете атомарность выполнения действия и выброса события. Отдельный класс `Message Relay` вытаскивает сохраненные события из базы и дальше оборачивает их в сообщение и отправляет асинхронный транспорт (тот же раббит). Уже консьюмер сообщения рабита вытаскивает из него событие и асинхронно обрабатывает его.
Эта статья - первая часть большой статьи про Messenger, которую я решил поделить. Во второй части я подробно на примере покажу, как работает транзакционный обмен сообщениями.
Спасибо за Ваш комментарий.) Я потому и поставил уровень статьи "легкий".) Если человек понимает разницу, то ему статья с объяснением этой разницы будет бесполезна.) Но беда в том, что я встречал очень многих разработчиков, которые этой разницы не понимают, и это воплощалось в их коде.
Возможно, я слишком большой акцент сделал на различии понятий и слишком малый - на архитектуре. Идея-то была показать, где, в каком слое, в проекте с хорошей архитектурой место события, где - его обработчиков, а где - транспорта.
Основная мысль, которая, возможно, утонула в лингвистически разборах, о которых Вы говорите, состояла в том, что доменное событие, прежде чем дойти до внешнего консьюмера, проходит длинный путь преобразования: Доменное событие -> Domain (!) Event Dispatcher -> Обработчик -> Message (перзистентное) -> Relay -> сериалайзер Symfony Messenger -> Транспорт Symfony Messenger -> Rabbit MQ и далее в консьюмере обратный путь через десереализацию к хандлеру, который уже совсем не доменный.
Это, на самом деле, первая часть большой статьи, которую я решил разделить. Спасибо, что подсветили, во второй части сделаю прям больший упор на архитектуру, код и путь движения события через слои.
Да, сам факт необходимости в таких людях, конечно не оспаривается.) Я это писал к тому, что когда проектируешь какой-то кусок, постоянно приходится думать: вот придут ребята, начнут его дописывать по-своему, использовать по-своему, а кто-то не увидит и сделает похожее.
Эти вопросы всегда висят в воздухе и постоянно приходится думать, как реализовать удобное для переиспользования и сопровождения решение. Желательно так, чтобы вообще не пришлось объяснять, что это и зачем. И уж тем более, чтобы не следить потом зорким глазом на ревью, ударяя по рукам всякому, кто отклонился от твоего замысла.)
Да, согласен с вами. Единственно, про CQRS не понял, причем он здесь. В статье слово агрегат применялось не в смысле доменного агрегата, а скорее как агрегируемое из разных источников представление кампании.
Спасибо за ваш отзыв! Метод supports будет первым вызван в самом "внешнем" трансформере-декораторе. Чтобы это представить наглядно, вы можете обратиться к последней схеме в статье: там где цепочка трансформеров. Если дорисовывать эту схему декораторами, то мы могли бы какой-то один из эллипсов на схеме обвести еще одним эллипсом. Вот этот второй, внешний эллипс - и есть трансформер-декоратор.
Соответственно, именно самый внешний трансформер примет запросы на выполнение операций
supports()
иtransform()
. Поскольку, при написании каждого класса трансформера, мы не знаем, как именно он будет использован в клиентском коде, то есть, будет он единственным трансформером, или в составе цепочки, будет он снаружи или внутри "многослойного" звена, нам следует реализовать методы supports() и transform() в каждом классе.Вдобавок, своей реализацией метода
supports()
трансформер-декоратор дополнительно подчеркивает, что не может обходиться безnested
трансформера и служит лишь для добавления каких-то дополнительных возможностей, а не в качестве самостоятельного компонента.Спасибо за ваш вариант решения. Разумеется, любую задачу можно решить множеством способов, и каждый имеет право на жизнь.) Что лично меня здесь настораживает:
Разные потребители требуют разный состав данных, то есть не для каждого случая есть необходимость загружать весь агрегат. Лучше иметь возможность управлять тем, какие именно части агрегата будут загружены в зависимости от потребности потребителя.
Подход "один потребитель - один трансформер" - это как раз то, чего я стремился избежать, чтобы не дублировать код. Хотелось иметь набор переиспользуемых мини-трансформеров и из них уже набирать конечное решение для каждого потребителя.
Я в целом согласен с тем, что итоговое решение не блещет простотой. Но зато оно блещет очень большой гибкостью, что и было целью создания этого микросервиса.)
Не знаю, мне давно хотелось рассказать об этом интересном кейсе: как пошаговое решение задачи само по себе приводило к добавлению все новых паттернов.)
Применение ООП ни к месту для меня звучит как оксюморон. Что же, делать проекты в процедурном стиле и задумываться над тем, достоин ли вот этот проект применения ООП?
На счет понимания самих принципов: бьюсь в каждой команде, где случалось работать, за то, чтобы ребята начали лучше их понимать. Скажу, что это окупается и отражается на коде в повседневной работе. Не понимаю, что такое ООП ни к месту.
На счет паттернов: да, здесь можно находить кейсы применения ни к месту. Но обычно это связано с непониманием самого паттерна и того же полиморфизма. Чаще это ситуация, когда человек заявляет, что у него реализован паттерн, а на самом деле его там и в помине нет. Такого, чтоб паттерн реализован правильно, и это было ни к месту, нечасто видел.
Да, все верно.)
Тут зависит от того, что вы понимаете под принадлежностью юзера к persistence layer. Если у вас на класс
User
завязана какая-то бизнес-логика, и вы, например, разметили его доктриновскими аннотациями, то это - тоже смешение инфраструктурного и доменного слоев: вы добавили в доменную сущность метаданные о том, как эта сущность хранится.В остальном, в принципе, наши с вами мысли схожи: разделяем класс юзера на два и каждый знает свое место: один помещается в инфраструктурный слой, второй - в доменный. Вы говорите о других видах слоев, но суть остается прежней.
Да, все верно. Трагедии нет. Просто, как я писал, у каждого решения есть плюсы (профиты) и минусы (цена). Соответственно, не упомянуть об этом профите (уменьшение запросов в базу) при описании решения было бы, на мой взгляд, неправильно.)
Да, есть такой минус у этого решения: к проектированию payload нужно подходить ответственно и думать о том, как сжимать токен. Способы для этого я упомянул.
Вспомнил мультик советский, где героя отправляли к двум дядькам из ларца.
Вы хотите получить универсальное решение, чтобы можно было его в любой проект вставлять, и больше не думать об этом никогда. Если набор таких решений когда-то появится, то поголовье нас, разработчиков, сильно сократиться.
Логика, на основе которой принимается решение о доступе, диктуется бизнесом. В одном проекте эта система
ManagerAuthorities
с разными видами и уровнями менеджеров. В другом - это система членств в разных группах. В третьем - система тегов, которыми овнеры(админы) размечают создаваемых ими юзеров.В этом и состоит работа разработчика: понять требования бизнеса, спроектировать решение и реализовать его так, чтобы было комфортно с ним жить как можно дольше.
А вы пишите, что вы устали, читая статью и разочарованы тем, что ваш труд по прочтению не был вознагражден таким результатом, что вы навсегда будете избавлены от необходимости думать, на основании чего вам принимать решение.
Попробуйте мыслить атрибутами, а не ролями.
Обратите внимание на тот абзац, где я писал про вынос всего, что касается Security, в отдельный composer пакет.
Вообще, пока писал ответ на ваш комментарий, вспомнил о тех ребятах, которые требуют им расписывать задачу до такой степени детализации, что сделать самому эту задачу выходит в несколько раз быстрее.
Своя ролевая система для каждого микросервиса - звучит как путь, полный боли.) Одну-то тежяло в порядке поддерживать, не давая ей разрастаться.
Да, только если делать set-cookie в ответе на каждый запрос, то мы возвращаемся к началу: придется каждый раз генерировать JWT, лазая в базу.
Единственно, если фронт по каким-то причинам не сохранит полученный JWT, то будет ломиться со старым токеном. Так что, уйти таким способом от проверки актуальности токена, как я вначале подумал, не получится, увы.
В любом случае, Вы накинули свежего взгляда, есть над чем подумать.)
Отвечать новым токеном на запрос о вступлении в группу - хорошая идея! Спасибо!
Ваши комментарии еще поосмысляю, спасибо, что пишите их. Пока пару моментов могу сказать:
Перевыпуск токена на фронте можно сделать без слежки за временем, а просто как реакцию на 401 ответ. Фронты на это спокойно смотрят.)
На счет проверки токена на необходимость отзыва раз в n минут. Это не будет работать в ситуации, которую я описал в статье: юзер вступил в группу по приглашению и сразу редиректится на страницу со списком материалов группы. При проверке токена раз в n минут юзер будет ждать эти самые n минут, получая 403 в ответ на попытки увидеть материалы группы.
То есть, здесь надо будет более тонкую логику какую-то делать. Пока еще соображаю.)
JWT, как правило, выдаются людям, а люди редко имеют статические IP-адреса. Технически-то легко сделать: добавляете IP в payload токена и проверяете при каждом запросе. Только смысл это будет иметь, если клиентами вашего приложения являются не люди, а другие приложения, работающие на статических IP-адресах.
Возможно, я просто опять Вас не так понял и мы снова говорим про разное.) Обычная схема какая: Выдается два токена: собственно JWT и refresh_token. Когда срок жизни JWT истекает, фронт (или иной клиент) автоматически запрашивает новый токен по refresh токену.
Предполагается, что если злоумышленник перехватит JWT, то он сможет пользоваться им только до истечения его срока годности, а потом, не имея refresh токена, он потеряет доступ к приложению.
При таких условиях чем меньше срок жизни JWT, тем считается безопаснее. При этом автоматический перезапрос JWT по refresh токену, осуществляемый фронтом, проходит для пользователя незаметно: его не выкидывает на страницу логина.
Прошу прощения. Я подумал, что Вы критикуете меня в форме искаженного пересказа моего текста.) Теперь, когда Вы указали, что это Вы описали свое, третье, решение, я взглянул на Ваш комментарий по-новому. Мой косяк.)
Ваше решение тоже интересно, еще подумаю над ним. Идея проверять на отзыв JWT не при каждом запросе, а раз в несколько минут интересна, тут, конечно, бизнес-требования каждого конкретного проекта будут вносить свои коррективы.
Единственно, что хотелось бы отметить, так это то, что годовалый срок жизни токена считается плохой практикой с точки зрения информационной безопасности.
Спасибо большое, что поделились своим решением.)