Можно ли обсуждать свою ЗП?
Нет, у меня в команде это табу!
1️⃣ Почему
Мы все очень разные, некоторым разница в ЗП в 50-100к не имеет никакого значения, т.к. человеку достаточно того, что он имеет, и он чувствует комфорт. Для другого же человека разница в 10-20к - это уже повод начать выяснять, почему. Именно такой пример был у меня в практике.
2️⃣ Зачем
Я не понимаю, зачем кому-то знать ЗП в коллективе, чтобы что? Ну, раз у всех небольшая ЗП, значит, я потерплю? Или раз у всех большая ЗП, значит, я тоже хочу такую?
Узнать среднюю ЗП на свой грейд не очень сложно - идешь на любой агрегатор вакансий и смотришь вакансии по своему грейду. А реальную стоимость себя узнаешь только на собеседованиях.
3️⃣Что же делать
В бигтехе есть грейды, там всё с виду ясно, но не все так просто. Знаю примеры, когда грейд повысил, все это признают, но ЗП повышать не спешат. Может, к следующему перфоманс-ревью, но пока возьми на себя ответственности побольше.
Если в небольшой компании - все вопросы к лиду (например, на 1 на 1). Лид должен поставить цели на какой-то период, по исполнении которых должна повышаться ЗП.
Нет, у меня в команде это табу!
1️⃣ Почему
Мы все очень разные, некоторым разница в ЗП в 50-100к не имеет никакого значения, т.к. человеку достаточно того, что он имеет, и он чувствует комфорт. Для другого же человека разница в 10-20к - это уже повод начать выяснять, почему. Именно такой пример был у меня в практике.
2️⃣ Зачем
Я не понимаю, зачем кому-то знать ЗП в коллективе, чтобы что? Ну, раз у всех небольшая ЗП, значит, я потерплю? Или раз у всех большая ЗП, значит, я тоже хочу такую?
Узнать среднюю ЗП на свой грейд не очень сложно - идешь на любой агрегатор вакансий и смотришь вакансии по своему грейду. А реальную стоимость себя узнаешь только на собеседованиях.
3️⃣Что же делать
В бигтехе есть грейды, там всё с виду ясно, но не все так просто. Знаю примеры, когда грейд повысил, все это признают, но ЗП повышать не спешат. Может, к следующему перфоманс-ревью, но пока возьми на себя ответственности побольше.
Если в небольшой компании - все вопросы к лиду (например, на 1 на 1). Лид должен поставить цели на какой-то период, по исполнении которых должна повышаться ЗП.
👏2👎1🫡1
Стратегические паттерны DDD
Последним постом про Dependency Inversion мы закончили краткий обзор принципов SOLID. Я очень надеюсь, что вы пошли немного дальше и постарались разобраться с каждым принципом внимательнее, по крайней мере, почитали ссылки, которые я приводил.
Теперь хотелось бы перейти к теме посерьезнее - Domain Driven Design. Я думаю тут не получится обойтись краткостью, и иногда придется возвращаться назад, что-то дополнять, возможно, переосмысливать. Признаюсь честно, не все темы в DDD мне кристально ясны, некоторые спорны, обсудим и это также.
Итак, давайте для начала представим идеальный путь разработки приложения с использованием DDD.
В этом посте пройдемся вкратце по стратегическим паттернам DDD.
1️⃣ Предметная область
От классика: это любая область знаний.
На этом этапе нам нужно понять, как разрабатываемая часть работает в действительности, в бизнесе. С этим нам помогают эксперты предметной области: бизнес-аналитики, конечные пользователи, сам бизнес, который пришел к нам за приложением. Еще раз: здесь мы собираем требования к системе.
2️⃣ Единый язык
В процессе взаимодействия с экспертами мы слушаем, какие слова произносит эксперт, и используем их в коде! Если эксперт говорит "клиент" - у нас в коде должен быть класс Client, не Customer, не User. Код отражает язык бизнеса, а не наоборот!
Не только лишь классы называем так, как говорит эксперт, но и методы классов - это также какие-то действия, которые мы слышим от эксперта.
3️⃣ Ограниченные контексты
Если сильно упростить, то это отдельные логические части приложения. В каждом ограниченном контексте своя собственная модель данных, свой единый язык и правила.
Хорошим маркером отдельного контекста будет, когда придет эксперт и скажет, что-то про пользователей (User), не клиентов (Client), и окажется, что есть некоторые отличия между клиентом и пользователем. Вот именно тут происходит разделение контекста. Или еще глубже - когда для разных частей системы понятие клиента значит разное.
4️⃣ Домены / Субдомены
Эванс использует эти понятия как взаимозаменяемые. Это классификация бизнес-логики по важности для бизнеса:
🔸Core Domain: дает конкурентные преимущества, сложный, разрабатывается внутри компании самыми квалифицированными кадрами. Чаще всего реализуется в отдельном ограниченном контексте.
🔸Supporting Domain: вспомогательная логика, специфичная для бизнеса, но не универсальная. Чаще всего CRUDы, что-то не очень сложное. Часто могут быть внутри какого-то контекста.
🔸Generic Domain: что-то универсальное, то, что все компании делают одинаково. Логи, авторизация. Часто отдельный контекст - использование готового решения.
Связь между ограниченным контекстом и доменом не так проста, вынесем это в отдельный пост.
5️⃣ Стратегия DDD
🔸Выделяем Core Domain и строим вокруг него всю систему.
🔸Изолируем его от Supporting и Generic через контексты.
🔸Управляем взаимодействием контекстов через Context Mapping.
Ссылки: https://t.me/heart2code/37
#DDD
Последним постом про Dependency Inversion мы закончили краткий обзор принципов SOLID. Я очень надеюсь, что вы пошли немного дальше и постарались разобраться с каждым принципом внимательнее, по крайней мере, почитали ссылки, которые я приводил.
Теперь хотелось бы перейти к теме посерьезнее - Domain Driven Design. Я думаю тут не получится обойтись краткостью, и иногда придется возвращаться назад, что-то дополнять, возможно, переосмысливать. Признаюсь честно, не все темы в DDD мне кристально ясны, некоторые спорны, обсудим и это также.
Итак, давайте для начала представим идеальный путь разработки приложения с использованием DDD.
В этом посте пройдемся вкратце по стратегическим паттернам DDD.
1️⃣ Предметная область
От классика: это любая область знаний.
На этом этапе нам нужно понять, как разрабатываемая часть работает в действительности, в бизнесе. С этим нам помогают эксперты предметной области: бизнес-аналитики, конечные пользователи, сам бизнес, который пришел к нам за приложением. Еще раз: здесь мы собираем требования к системе.
2️⃣ Единый язык
В процессе взаимодействия с экспертами мы слушаем, какие слова произносит эксперт, и используем их в коде! Если эксперт говорит "клиент" - у нас в коде должен быть класс Client, не Customer, не User. Код отражает язык бизнеса, а не наоборот!
Не только лишь классы называем так, как говорит эксперт, но и методы классов - это также какие-то действия, которые мы слышим от эксперта.
3️⃣ Ограниченные контексты
Если сильно упростить, то это отдельные логические части приложения. В каждом ограниченном контексте своя собственная модель данных, свой единый язык и правила.
Хорошим маркером отдельного контекста будет, когда придет эксперт и скажет, что-то про пользователей (User), не клиентов (Client), и окажется, что есть некоторые отличия между клиентом и пользователем. Вот именно тут происходит разделение контекста. Или еще глубже - когда для разных частей системы понятие клиента значит разное.
4️⃣ Домены / Субдомены
Эванс использует эти понятия как взаимозаменяемые. Это классификация бизнес-логики по важности для бизнеса:
🔸Core Domain: дает конкурентные преимущества, сложный, разрабатывается внутри компании самыми квалифицированными кадрами. Чаще всего реализуется в отдельном ограниченном контексте.
🔸Supporting Domain: вспомогательная логика, специфичная для бизнеса, но не универсальная. Чаще всего CRUDы, что-то не очень сложное. Часто могут быть внутри какого-то контекста.
🔸Generic Domain: что-то универсальное, то, что все компании делают одинаково. Логи, авторизация. Часто отдельный контекст - использование готового решения.
Связь между ограниченным контекстом и доменом не так проста, вынесем это в отдельный пост.
5️⃣ Стратегия DDD
🔸Выделяем Core Domain и строим вокруг него всю систему.
🔸Изолируем его от Supporting и Generic через контексты.
🔸Управляем взаимодействием контекстов через Context Mapping.
Ссылки: https://t.me/heart2code/37
#DDD
Telegram
От сердца к коду
Для меня есть 4 базовых книги и 3 автора по DDD:
🔸Влад Хононов - Изучаем DDD
🔸Вон Вернон - Реализация методов предметно-ориентированного проектирования
🔸Вон Вернон - Предметно - ориентированное проектирование. Самое основное
🔸Эрик Эванс - Предметно-ориентированное…
🔸Влад Хононов - Изучаем DDD
🔸Вон Вернон - Реализация методов предметно-ориентированного проектирования
🔸Вон Вернон - Предметно - ориентированное проектирование. Самое основное
🔸Эрик Эванс - Предметно-ориентированное…
👍3🫡3🔥1
Тактические паттерны DDD
В прошлом посте мы теоретически собрали требования от экспертов доменной области, используя единый язык. Разделили приложение на ограниченные контексты и выделили Core Domain.
В этой части сделаем краткий обзор на то, как теперь это все закодить.
1️⃣ Моделирование домена
С чего же нужно начинать разработку?
По незнанию и при нехватке квалификации разработка начинается с проектирования БД, консюмеров Kafka, выбора фреймворка. То есть выбираем технологии. Это абсолютно неправильно.
Разработка всегда начинается с модели данных. Простыми словами - проектирование классов и их поведения без привязки к технологиям. Новый проект начинается с проектирования модели Core Domain, используя простые классы и единый язык.
Пусть у нас не новый проект - это ничего не меняет: сначала исправляем (дорабатываем) модель данных, а потом уже думаем про репозитории, консюмеры и т.п.
2️⃣ Паттерны моделирования
🔸Entities (Сущности): объекты с уникальной идентичностью (у них всегда есть поле ID).
🔸Value Objects (Объекты-значения): неизменяемые объекты, характеристики Entity.
🔸Aggregates (Агрегаты): группа связанных Entities и Value Objects, управляемых через корневой объект (сложная тема, вот тут сам пока еще только дохожу до понимания).
🔸Domain Events (Доменные события): события изменений в модели данных.
🔸Domain Services (Доменные сервисы): компоненты, реализующие логику, не принадлежащую конкретной сущности или объекту-значению. Например, операции, затрагивающие несколько агрегатов или сущностей. Инкапсуляция сложных бизнес-правил. О разнице между Domain Service, Application Service и Usecase я расскажу в отдельном посте.
🔸Repositories (Репозитории): интерфейсы для доступа к агрегатам (сущностям).
Используя описанные выше паттерны, первым шагом разработки всегда будет проектирование модели предметной области.
3️⃣ Выбор архитектуры
Набросок модели готов, и приходит время подумать про выбор технологий, инфраструктуру.
Для себя я выделяю, по сути, 3 подхода к организации архитектуры:
🔸Hexagonal Architecture (Ports & Adapters)
🔸Clean Architecture
🔸CQRS (Command Query Responsibility Segregation)
Выбор влияет и на то, как будет выглядеть структура директорий проекта. Об этом поговорим отдельно.
4️⃣ Документация
Если говорить в контексте DDD, то обязательно должна быть актуальная документация на: карты контекстов и глоссарий единого языка.
На этом все, дальше уже будем детальнее обсуждать какие-то части DDD.
#DDD
В прошлом посте мы теоретически собрали требования от экспертов доменной области, используя единый язык. Разделили приложение на ограниченные контексты и выделили Core Domain.
В этой части сделаем краткий обзор на то, как теперь это все закодить.
1️⃣ Моделирование домена
С чего же нужно начинать разработку?
По незнанию и при нехватке квалификации разработка начинается с проектирования БД, консюмеров Kafka, выбора фреймворка. То есть выбираем технологии. Это абсолютно неправильно.
Разработка всегда начинается с модели данных. Простыми словами - проектирование классов и их поведения без привязки к технологиям. Новый проект начинается с проектирования модели Core Domain, используя простые классы и единый язык.
Пусть у нас не новый проект - это ничего не меняет: сначала исправляем (дорабатываем) модель данных, а потом уже думаем про репозитории, консюмеры и т.п.
2️⃣ Паттерны моделирования
🔸Entities (Сущности): объекты с уникальной идентичностью (у них всегда есть поле ID).
🔸Value Objects (Объекты-значения): неизменяемые объекты, характеристики Entity.
🔸Aggregates (Агрегаты): группа связанных Entities и Value Objects, управляемых через корневой объект (сложная тема, вот тут сам пока еще только дохожу до понимания).
🔸Domain Events (Доменные события): события изменений в модели данных.
🔸Domain Services (Доменные сервисы): компоненты, реализующие логику, не принадлежащую конкретной сущности или объекту-значению. Например, операции, затрагивающие несколько агрегатов или сущностей. Инкапсуляция сложных бизнес-правил. О разнице между Domain Service, Application Service и Usecase я расскажу в отдельном посте.
🔸Repositories (Репозитории): интерфейсы для доступа к агрегатам (сущностям).
Используя описанные выше паттерны, первым шагом разработки всегда будет проектирование модели предметной области.
3️⃣ Выбор архитектуры
Набросок модели готов, и приходит время подумать про выбор технологий, инфраструктуру.
Для себя я выделяю, по сути, 3 подхода к организации архитектуры:
🔸Hexagonal Architecture (Ports & Adapters)
🔸Clean Architecture
🔸CQRS (Command Query Responsibility Segregation)
Выбор влияет и на то, как будет выглядеть структура директорий проекта. Об этом поговорим отдельно.
4️⃣ Документация
Если говорить в контексте DDD, то обязательно должна быть актуальная документация на: карты контекстов и глоссарий единого языка.
На этом все, дальше уже будем детальнее обсуждать какие-то части DDD.
#DDD
🫡2👍1
Лучше потратить два дня на Event Storming, чем два месяца на переписывание кода
В комментариях к прошлому посту мой коллега Дима вполне рационально спросил - зачем лезть в код до проектирования концептуальной модели? И он прав, я использовал упрощение, но в идеальном мире, прежде чем садиться за код, нужно как-то спроектировать систему.
1️⃣ Что это такое
Event Storming - это метод проектирования и анализа сложных систем, который очень хорошо ложится на концепцию DDD. Если еще точнее, на DDD + CQRS.
В процессе проектирования системы выделяются ивенты, агрегаты, команды и ограниченные контексты. Как раз то, что нам нужно.
У меня был реальный опыт проектирования интеграционного фреймворка через Event Storming. Мы собрались в Zoom: несколько разработчиков, CTO и PO (эксперты предметной области). Открыли общую доску в Miro и просто пошли по шагам. Заняло у нас это примерно 4-5 сессий по 2 часа. И на выходе получили архитектуру фреймворка из 5 микросервисов (ограниченные контексты), агрегаты в них, команды (единый язык) и ивенты между ними. Оставалось самое простое - закодить.
2️⃣ Преимущества Event Storming
🔸Проектирование архитектуры. Можно использовать как для новых систем, так и для рефакторинга старых.
🔸Устраняет разрыв между бизнесом и разработкой. Все начинают говорить на одном языке.
🔸Снижает риски. Раннее обнаружение проблемных мест.
3️⃣ Шаги
Ниже будут 10 шагов проведения Event Storming, взятых из книги Влада Хононова - Изучаем DDD. За подробностями обращаемся к ней, гуглим.
- Собираем все возможные доменные события в данном бизнес-домене.
- Выстраиваем последовательность этих событий, в которой они появляются.
- Выделяем "болевые" точки - места, в которых нам не хватает знаний.
- Выделяем переходы между состояниями, фазами, контекстами.
- Выделяем команды (операции), которые запускают ивенты из 1 пункта.
- Выделяем инварианты/правила.
- Выделяем представления для акторов.
- Выделяем внешние системы.
- Объединяем команды и ивенты, относящиеся к общему агрегату.
- Выделяем ограниченные контексты.
На выходе получается готовая, разноцветная схема.
4️⃣ Читаем
- https://habr.com/ru/companies/oleg-bunin/articles/537862/
- https://contextmapper.org/
- Влад Хононов, "Изучаем DDD"
- Альберто Брандани, "Introducing EventStorming"
#DDD
👍3🫡2👏1
Связь ограниченных контекстов и доменов
1️⃣ Домены
Это предметная область бизнеса, то, чем он занимается. Они выявляются анализом работы компании и характеризуют важность бизнес-логики.
🔸Core Domain (основной): уникальная ценность компании, то, что отличает ее от конкурентов.
Главная задача - изолировать основной домен от других типов, т.к. он сложнее и чаще всего изменяется. Изолировать - не всегда значит перенести в микросервис, иногда это логическое разделение внутри монолита.
Разрабатывается всегда внутри компании. Именно здесь должны быть самые большие инвестиции, предназначенные для разработки ПО.
🔸Supporting Subdomain (вспомогательный): простая бизнес-логика, не уникальная для бизнеса, напоминает ETL-процессы или CRUDы.
Не дает конкурентных преимуществ, имеет низкую сложность, можно отдавать на аутсорсинг.
🔸Generic Subdomain (универсальный): общие задачи, которые можно решить готовыми решениями.
Имеет высокую сложность, ищем что-то готовое, дешевле купить, чем писать самим.
Поддомен — это конкретная область знаний или деятельности, которую необходимо смоделировать в ПО.
2️⃣ Ограниченный контекст
Это техническая граница, внутри которой: используется единый язык, реализуется модель (сущности, сервисы), и эта модель не имеет противоречивых значений.
В двух разных контекстах один и тот же термин может иметь разное значение, структуру и поведение.
Ограниченные контексты проектируются!
Ограниченный контекст должен принадлежать только одной команде разработки! У одной команды может быть несколько ограниченных контекстов, но нельзя, чтобы несколько команд разрабатывали один контекст.
Ограниченные контексты (Bounded Contexts) — логические части домена, где:
- Определенные термины (Ubiquitous Language) имеют конкретный смысл,
- Модели данных (Entities, Value Objects) адаптированы под задачи контекста.
- Могут быть реализованы как: микросервисы, модули в монолите или отдельные либы.
3️⃣ Связь домена и ограниченного контекста
🔸Каждый поддомен может быть реализован в одном или нескольких ограниченных контекстах.
🔸Один контекст может охватывать часть или поддомен целиком.
🔸Один контекст может содержать домены нескольких типов: например, Payment контекст может включать и core-логику (эксклюзивный метод оплаты) и generic-логику (например интеграцию с СБП)
4️⃣ Итого
Домен реализуется через один или несколько ограниченных контекстов, каждый из которых имеет свою модель данных и четкие границы. Это позволяет избежать конфликтов в терминах и структурах.
Поддомены - это стратегическое разделение по бизнес-ценности, а контексты - тактическое разделение по технической реализации.
В следующем посте продолжим про интеграцию ограниченных контекстов.
#DDD
1️⃣ Домены
Это предметная область бизнеса, то, чем он занимается. Они выявляются анализом работы компании и характеризуют важность бизнес-логики.
🔸Core Domain (основной): уникальная ценность компании, то, что отличает ее от конкурентов.
Главная задача - изолировать основной домен от других типов, т.к. он сложнее и чаще всего изменяется. Изолировать - не всегда значит перенести в микросервис, иногда это логическое разделение внутри монолита.
Разрабатывается всегда внутри компании. Именно здесь должны быть самые большие инвестиции, предназначенные для разработки ПО.
🔸Supporting Subdomain (вспомогательный): простая бизнес-логика, не уникальная для бизнеса, напоминает ETL-процессы или CRUDы.
Не дает конкурентных преимуществ, имеет низкую сложность, можно отдавать на аутсорсинг.
🔸Generic Subdomain (универсальный): общие задачи, которые можно решить готовыми решениями.
Имеет высокую сложность, ищем что-то готовое, дешевле купить, чем писать самим.
Поддомен — это конкретная область знаний или деятельности, которую необходимо смоделировать в ПО.
2️⃣ Ограниченный контекст
Это техническая граница, внутри которой: используется единый язык, реализуется модель (сущности, сервисы), и эта модель не имеет противоречивых значений.
В двух разных контекстах один и тот же термин может иметь разное значение, структуру и поведение.
Ограниченные контексты проектируются!
Ограниченный контекст должен принадлежать только одной команде разработки! У одной команды может быть несколько ограниченных контекстов, но нельзя, чтобы несколько команд разрабатывали один контекст.
Ограниченные контексты (Bounded Contexts) — логические части домена, где:
- Определенные термины (Ubiquitous Language) имеют конкретный смысл,
- Модели данных (Entities, Value Objects) адаптированы под задачи контекста.
- Могут быть реализованы как: микросервисы, модули в монолите или отдельные либы.
3️⃣ Связь домена и ограниченного контекста
🔸Каждый поддомен может быть реализован в одном или нескольких ограниченных контекстах.
🔸Один контекст может охватывать часть или поддомен целиком.
🔸Один контекст может содержать домены нескольких типов: например, Payment контекст может включать и core-логику (эксклюзивный метод оплаты) и generic-логику (например интеграцию с СБП)
4️⃣ Итого
Домен реализуется через один или несколько ограниченных контекстов, каждый из которых имеет свою модель данных и четкие границы. Это позволяет избежать конфликтов в терминах и структурах.
Поддомены - это стратегическое разделение по бизнес-ценности, а контексты - тактическое разделение по технической реализации.
В следующем посте продолжим про интеграцию ограниченных контекстов.
#DDD
1🫡4👍2
Интеграция ограниченных контекстов (часть 1)
Встает вопрос: как быть если у нас один домен разрабатывается в нескольких контекстах? Как организовать взаимодействие между этими контекстами?
В целом, каждый ограниченный контекст должен иметь свою собственную модель данных: свои сущности, события. Но что, если кажется, что одна модель данных ложится на несколько контекстов?
1️⃣ Валидация модели
Точно ли одна сущность подходит к двум контекстам? А не хочется ли в нее добавить полей, специфичных для какого-то одного контекста?
🔸Пусть даже названия совпадают, но семантически это могут быть разные сущности.
🔸Пусть каждый контекст имеет свою сущность.
🔸Кроме того, не запрещено для связей использовать идентификаторы (user_id)
2️⃣ Shared Kernel (Общее ядро)
Действительно, бывают случаи, когда одна модель данных подходит на несколько доменов. Тогда выносим общие сущности в отдельный модуль.
Не забываем о последствиях:
🔸Изменения в Shared Kernel влияют на все контексты.
🔸Риск возникновения скрытой связанности.
🔸Допустимо внутри одной команды.
🔸Подходит для стабильной, редко изменяемой логики.
🔸Хорошо подходит для Core Domain.
3️⃣ Дублирование с преобразованием
Каждый контекст имеет свою копию или очень близкую к копии версию сущности. Данные синхронизируются через ивенты.
🔸Можно применять только, когда контексты разрабатываются разными командами.
🔸Огромные расходы на синхронизацию.
4️⃣ Выделение отдельного ограниченного контекста
Возможно, стоит задуматься над выделением общей сущности в отдельный ограниченный контекст.
Как пример, сущность Address можно выделить в отдельный контекст Геоданных.
Заключение
В это части мы разобрали, как быть когда очень хочется использовать общую доменную модель. Но ведь существуют интеграции, когда такое невозможно. Об этой группе паттернов мы поговорим с следующем посте.
#DDD
Встает вопрос: как быть если у нас один домен разрабатывается в нескольких контекстах? Как организовать взаимодействие между этими контекстами?
В целом, каждый ограниченный контекст должен иметь свою собственную модель данных: свои сущности, события. Но что, если кажется, что одна модель данных ложится на несколько контекстов?
1️⃣ Валидация модели
Точно ли одна сущность подходит к двум контекстам? А не хочется ли в нее добавить полей, специфичных для какого-то одного контекста?
🔸Пусть даже названия совпадают, но семантически это могут быть разные сущности.
🔸Пусть каждый контекст имеет свою сущность.
🔸Кроме того, не запрещено для связей использовать идентификаторы (user_id)
2️⃣ Shared Kernel (Общее ядро)
Действительно, бывают случаи, когда одна модель данных подходит на несколько доменов. Тогда выносим общие сущности в отдельный модуль.
Не забываем о последствиях:
🔸Изменения в Shared Kernel влияют на все контексты.
🔸Риск возникновения скрытой связанности.
🔸Допустимо внутри одной команды.
🔸Подходит для стабильной, редко изменяемой логики.
🔸Хорошо подходит для Core Domain.
3️⃣ Дублирование с преобразованием
Каждый контекст имеет свою копию или очень близкую к копии версию сущности. Данные синхронизируются через ивенты.
🔸Можно применять только, когда контексты разрабатываются разными командами.
🔸Огромные расходы на синхронизацию.
4️⃣ Выделение отдельного ограниченного контекста
Возможно, стоит задуматься над выделением общей сущности в отдельный ограниченный контекст.
Как пример, сущность Address можно выделить в отдельный контекст Геоданных.
Заключение
В это части мы разобрали, как быть когда очень хочется использовать общую доменную модель. Но ведь существуют интеграции, когда такое невозможно. Об этой группе паттернов мы поговорим с следующем посте.
#DDD
🔥3👍2🫡2
Интеграция ограниченных контекстов (часть 2)
В прошлом посте мы разобрали, как быть с общими доменными моделями. В этом же рассмотрим несколько паттернов для ситуаций, когда один ограниченный контекст предоставляет сервис другому.
1️⃣ Anti-Corruption Layer
Этот паттерн применяется со стороны получателя сервиса, того, кто использует внешний сервис и не хочет подстраиваться под модель поставщика.
Чтобы избежать использования внешней модели, сервис-получатель преобразует внешнюю модель в свою, подходящую ему.
Банальный пример: через API мы получаем модель Client, но внутри используется своя сущность Customer. ACL говорит о том, что нужно сделать маппинг на внутренние сущности, чтобы убрать зависимость от внешних.
Примеры использования:
🔸Поставщик - это легаси-система.
🔸Получатель - это Core Subdomain.
🔸Когда контракт поставщика часто изменяется, и нам нужно защитить свою модель от этого.
2️⃣ Open-Host Service
Этот паттерн - противоположный ACL. Тут сила на стороне получателя, т.е. поставщик заинтересован в предоставлении наилучшего сервиса.
Поставщик имеет две модели: одну - сложную для внутренних нужд, и вторую - подходящую для внешних интеграций. Т.е. преобразование между моделями происходит на стороне поставщика.
Обычно есть строгий контракт (OpenAPI или Protobuf), который поставщик старается не менять.
Хорошо подходит для Core-доменов, для скрытия своей сложности.
3️⃣ Domain Events
Классика микросервисов: контексты обмениваются информацией через асинхронные события, уведомляя о произошедших изменениях.
Хорошо подходит для слабосвязанных контекстов.
Заключение
Если два контекста используют «одну и ту же» сущность, но с разным поведением или данными — это разные сущности.
Следующим постом разберемся с Context Mapping.
#DDD
В прошлом посте мы разобрали, как быть с общими доменными моделями. В этом же рассмотрим несколько паттернов для ситуаций, когда один ограниченный контекст предоставляет сервис другому.
1️⃣ Anti-Corruption Layer
Этот паттерн применяется со стороны получателя сервиса, того, кто использует внешний сервис и не хочет подстраиваться под модель поставщика.
Чтобы избежать использования внешней модели, сервис-получатель преобразует внешнюю модель в свою, подходящую ему.
Банальный пример: через API мы получаем модель Client, но внутри используется своя сущность Customer. ACL говорит о том, что нужно сделать маппинг на внутренние сущности, чтобы убрать зависимость от внешних.
Примеры использования:
🔸Поставщик - это легаси-система.
🔸Получатель - это Core Subdomain.
🔸Когда контракт поставщика часто изменяется, и нам нужно защитить свою модель от этого.
2️⃣ Open-Host Service
Этот паттерн - противоположный ACL. Тут сила на стороне получателя, т.е. поставщик заинтересован в предоставлении наилучшего сервиса.
Поставщик имеет две модели: одну - сложную для внутренних нужд, и вторую - подходящую для внешних интеграций. Т.е. преобразование между моделями происходит на стороне поставщика.
Обычно есть строгий контракт (OpenAPI или Protobuf), который поставщик старается не менять.
Хорошо подходит для Core-доменов, для скрытия своей сложности.
3️⃣ Domain Events
Классика микросервисов: контексты обмениваются информацией через асинхронные события, уведомляя о произошедших изменениях.
Хорошо подходит для слабосвязанных контекстов.
Заключение
Если два контекста используют «одну и ту же» сущность, но с разным поведением или данными — это разные сущности.
Следующим постом разберемся с Context Mapping.
#DDD
👍5🫡3❤1
Context Mapping
Это карта взаимодействия ограниченных контекстов.
Из двух прошлых постов мы знаем, какие паттерны взаимодействия (интеграции) ограниченных контекстов бывают. Соответственно, теперь нам надо это перенести на диаграмму, в которой мы указываем:
🔸Сами ограниченные контексты.
🔸Зависимости между ними.
🔸Паттерны интеграций.
Зачем нужна карта контекстов:
🔸Понять сложность системы без погружения в код.
🔸Выбрать оптимальные способы интеграции, чтобы изменения в одном контексте не ломали другие.
🔸Получившаяся диаграмма становится общим языком для разработчиков, архитекторов и бизнес-экспертов.
Последнее, что можем обсудить - где рисовать? Да где удобнее: draw.io, contextmapper.org, dochub.info . Просто не забываем поддерживать актуальное состояние нашей диаграммы.
#DDD
Это карта взаимодействия ограниченных контекстов.
Из двух прошлых постов мы знаем, какие паттерны взаимодействия (интеграции) ограниченных контекстов бывают. Соответственно, теперь нам надо это перенести на диаграмму, в которой мы указываем:
🔸Сами ограниченные контексты.
🔸Зависимости между ними.
🔸Паттерны интеграций.
Зачем нужна карта контекстов:
🔸Понять сложность системы без погружения в код.
🔸Выбрать оптимальные способы интеграции, чтобы изменения в одном контексте не ломали другие.
🔸Получившаяся диаграмма становится общим языком для разработчиков, архитекторов и бизнес-экспертов.
Последнее, что можем обсудить - где рисовать? Да где удобнее: draw.io, contextmapper.org, dochub.info . Просто не забываем поддерживать актуальное состояние нашей диаграммы.
#DDD
👍2🔥1🫡1
Тактические паттерны DDD: Entity
Этим постом мы начинаем разбор тактических паттернов проектирования модели предметной области.
Тактические паттерны в DDD относятся напрямую к написанию кода. Чаще всего на собеседованиях, если вопрос заходит про DDD, разработчики рассказывают именно про тактику. Мне кажется, потому что эти паттерны доступнее понять, в то же время стратегические паттерны чаще недопоняты и недооценены.
Итак, что же такое Entity или сущность?
🔸Во-первых, это фундамент любого домена. С проектирования сущностей начинается разработка проекта.
🔸Главная цель сущности - отобразить поведение предметной области через свои методы.
🔸В коде сущность выглядит как класс или структура с полями, чьи типы в большинстве должны быть Value Object'ами (например, ClientID, ClientStatus).
🔸Сущность всегда имеет уникальный идентификатор - ClientID, по сути это обертка над UUID. Этот идентификатор не должен меняться в течение всей жизни сущности.
🔸Создается при помощи фабричного метода (NewClient()).
🔸Правильный подход говорит о том, что все поля сущности должны быть приватными. Вот почему:
- Always Valid Domain Model: нельзя просто так взять и поменять поле сущности, это может сломать ее работу.
- Второе: должна быть какая-то причина для изменения поля сущности. Как раз эта причина характеризуется поведением (методом) сущности (c.Block()).
🔸Не забываем о том, что Энтити должна существовать внутри определенного ограниченного контекста.
🔸Энтити не является отражением на БД!
🔸Сущность может передать своё поведение доменному сервису (Domain Service). Не путать с Application Service или Usecases. Как дойдем до них, объясняю в чем разница.
#DDD
Этим постом мы начинаем разбор тактических паттернов проектирования модели предметной области.
Тактические паттерны в DDD относятся напрямую к написанию кода. Чаще всего на собеседованиях, если вопрос заходит про DDD, разработчики рассказывают именно про тактику. Мне кажется, потому что эти паттерны доступнее понять, в то же время стратегические паттерны чаще недопоняты и недооценены.
Итак, что же такое Entity или сущность?
type ClientStatus string
func (s ClientStatus) String() string {
return string(s)
}
const (
New ClientStatus = "new"
Registered ClientStatus = "registered"
Blocked ClientStatus = "blocked"
)
type Client struct {
id ClientID
firstName string
lastName string
email Email
phone Phone
status ClientStatus
}
func NewClient(id ClientID, firstName, lastName string, email Email, phone Phone) (*Client, error) {
// какая-то валидация полей
if firstName == "" || lastName == "" {
return nil, errors.New("Имя и фамилия должны быть заданы")
}
if !phone.IsValid() {
return nil, errors.New("Задан неверный телефон")
}
return &Client{
id: id,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
status: New,
}, nil
}
func (c *Client) Block() error {
if c.status != Registered {
return errors.New("Заблокировать можно только в статусе registerd")
}
c.status = Blocked
}
🔸Во-первых, это фундамент любого домена. С проектирования сущностей начинается разработка проекта.
🔸Главная цель сущности - отобразить поведение предметной области через свои методы.
🔸В коде сущность выглядит как класс или структура с полями, чьи типы в большинстве должны быть Value Object'ами (например, ClientID, ClientStatus).
🔸Сущность всегда имеет уникальный идентификатор - ClientID, по сути это обертка над UUID. Этот идентификатор не должен меняться в течение всей жизни сущности.
🔸Создается при помощи фабричного метода (NewClient()).
🔸Правильный подход говорит о том, что все поля сущности должны быть приватными. Вот почему:
- Always Valid Domain Model: нельзя просто так взять и поменять поле сущности, это может сломать ее работу.
- Второе: должна быть какая-то причина для изменения поля сущности. Как раз эта причина характеризуется поведением (методом) сущности (c.Block()).
🔸Не забываем о том, что Энтити должна существовать внутри определенного ограниченного контекста.
🔸Энтити не является отражением на БД!
🔸Сущность может передать своё поведение доменному сервису (Domain Service). Не путать с Application Service или Usecases. Как дойдем до них, объясняю в чем разница.
#DDD
🔥1🫡1
Тактические паттерны DDD: Value Object
Начнем с примера простой сущности:
По мне, так абсолютно невыразительная сущность. Структура Клиент вообще никак себя не характеризует. Просто какой-то набор полей с примитивными типами.
Смотрим глубже: представим, что эти базовые типы, относящиеся к клиенту, будут везде: в аргументах юзкейсов, репозиториев и т.п. Печальная картина.
А теперь взглянем на сущность здорового человека:
А также некоторые выдуманные интерфейсы:
Какая красота (подумали вы). Вот это и есть Value Objects (VO сокращенно):
🔸Сущность характеризуется своими value-объектами. VO отражают бизнес-концепции.
🔸VO не имеет идентификатора.
🔸Является неизменяемым (immutable). Нельзя поменять поля структуры Address, придется пересоздать новый адрес. Следовательно код не должен содержать никаких сеттеров для value-объектов.
🔸Инкапсулирует в себя бизнес-логику и валидацию.
🔸Существует в контексте сущности.
🔸Если необходимо сравнить два VO, нужно сравнивать значения их полей.
🔸Для создания сложного VO - можно использовать фабричные методы с валидацией.
🔸Как VO должен храниться в БД?
- Поля VO это колонка в таблице сущности
- Поле jsonb (маршаллинг)
- Редко можно вынести в отдельную таблицу, но тогда не забываем, что для VO не должно быть своего репозитория, он загружается вместе с сущностью
#DDD
Начнем с примера простой сущности:
type Client struct {
id uuid
phone string
email string
address string
}По мне, так абсолютно невыразительная сущность. Структура Клиент вообще никак себя не характеризует. Просто какой-то набор полей с примитивными типами.
Смотрим глубже: представим, что эти базовые типы, относящиеся к клиенту, будут везде: в аргументах юзкейсов, репозиториев и т.п. Печальная картина.
А теперь взглянем на сущность здорового человека:
type Client struct {
id ClientID
phone Phone
email Email
address Address
}
type ClientID uuid
type Phone string
func (s Phone) String() string {
return string(s)
}
func (s Phone) Validate() error {
// логика по валидации
return nil
}
type Email string
func (s Email) String() string {
return string(s)
}
type Address struct {
street string
city string
country string
}
func NewAddress(street, city, country string) (Address, error) {
if street == "" || city == "" || country == "" {
return Address{}, errors.New("invalid address")
}
return Address{street, city, country}, nil
}
func (a Address) Street() string {
return a.street
}
func (a Address) IsEqual(addr Address) bool {
return a.street == addr.Street() &&
a.city == addr.City() &&
a.country == addr.Country()
}
А также некоторые выдуманные интерфейсы:
type ClientRepository interface {
Load(ctx context.Context, id ClientID) (*Client, error)
LoadByEmail(ctx context.Context, email Email) (*Client, error)
}Какая красота (подумали вы). Вот это и есть Value Objects (VO сокращенно):
🔸Сущность характеризуется своими value-объектами. VO отражают бизнес-концепции.
🔸VO не имеет идентификатора.
🔸Является неизменяемым (immutable). Нельзя поменять поля структуры Address, придется пересоздать новый адрес. Следовательно код не должен содержать никаких сеттеров для value-объектов.
🔸Инкапсулирует в себя бизнес-логику и валидацию.
🔸Существует в контексте сущности.
🔸Если необходимо сравнить два VO, нужно сравнивать значения их полей.
🔸Для создания сложного VO - можно использовать фабричные методы с валидацией.
🔸Как VO должен храниться в БД?
- Поля VO это колонка в таблице сущности
- Поле jsonb (маршаллинг)
- Редко можно вынести в отдельную таблицу, но тогда не забываем, что для VO не должно быть своего репозитория, он загружается вместе с сущностью
#DDD
👏2🫡2
Тактические паттерны DDD: Aggregate
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность это объект, то агрегат - это концепция (понятие) предметной области. Например, заказ или финансовая транзакция.
🔸Представляет собой кластер объектов: объединяет связанные сущности и value-объекты в одну логическую единицу. Другими словами, агрегат управляет состоянием связанных объектов.
🔸Order - это корень агрегата. Это значит, что всё взаимодействие должно идти только через него. Нельзя просто так взять и создать или модифицировать OrderItem. Агрегат отвечает за целостность данных.
🔸Во многих процессах есть непреклонные бизнес-правила, как например нельзя добавить вещь в заказ у которой цена или количество равны нулю. Такие бизнес-правила называются инвариантами. Эти инварианты должны отражаться в модели (агрегатах, сущностях, VO)
🔸Агрегат сохраняется в БД как единое целое, в транзакции. Кстати, загружаются тоже целиком.
🔸Изменение состояния возможно только через вызов методов. Как и в сущностях.
🔸Агрегат может быть связан с другим агрегатом через ID.
🔸Внимание: сущность или VO не может существовать отдельно от агрегата:
1. Каждый раз добавляя сущность или VO, думаем к какому агрегату он принадлежат.
2. Если об этом не подумать, то мы не сможем гарантировать валидное состояние системы.
3. Давайте примем факт, что возможны исключения. Например, для простых, самодостаточных сущностей. Сущность сама может быть агрегатом.
🔸Внимание 2: как понять надо ли объединять два агрегата? Если они никак не связаны инвариантами - это два разных агрегата.
🔸Может ли быть несколько агрегатов внутри одного контекста? Может, но их не должно быть много. Если в рамках одного контекста нужно изменить несколько агрегатов, это делается через разные транзакции или сагу.
Агрегаты в одном контексте должны быть высоко связаны! Должны решать какую-то общую задачу.
🔸Как взаимодействуют агрегаты в одном контексте? Связь через ID, я об этом говорю выше. Через доменные события или взаимодействие происходит в сервисах.
🔸Божество :) Дада, не надо делать божественные агрегаты, лучше несколько.
Накидывайте в комментах кто что думает, где я не прав? может что-то упустил? нам нужна истина!
Повторить: https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/aggregate.md
#DDD
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность это объект, то агрегат - это концепция (понятие) предметной области. Например, заказ или финансовая транзакция.
// Агрегат
type Order struct {
id OrderID
status Status
// Связь с другим агрегатом
customerID CustomerID
items []OrderItem
}
// Сущность
type OrderItem struct {
productID ProductID
quantity Quantity
price Money
}
// Value Object
type Money int64
func NewOrder(id OrderID, customerID CustomerID) *Order {
return &Order{
id: id,
status: New,
customerID: customerID,
items: make([]OrderItem, 0),
}
}
func (o *Order) AddItem(productID ProductID, quantity Quantity, price Money) error {
if quantity <= 0 {
return errors.New("quantity must be positive")
}
if price <= 0 {
return errors.New("price must be positive")
}
o.items = append(o.items, OrderItem{
productID: productID,
quantity: quantity,
price: price,
})
return nil
}
func (o *Order) Total() Money {
var total int64
for _, item := range o.items {
total += int64(item.price) * int64(item.quantity)
}
return Money(total)
}
func (o *Order) Cancel() error {
if o.status == Paid {
return errors.New("cannot cancel paid order")
}
o.status = Cancelled
return nil
}
🔸Представляет собой кластер объектов: объединяет связанные сущности и value-объекты в одну логическую единицу. Другими словами, агрегат управляет состоянием связанных объектов.
🔸Order - это корень агрегата. Это значит, что всё взаимодействие должно идти только через него. Нельзя просто так взять и создать или модифицировать OrderItem. Агрегат отвечает за целостность данных.
🔸Во многих процессах есть непреклонные бизнес-правила, как например нельзя добавить вещь в заказ у которой цена или количество равны нулю. Такие бизнес-правила называются инвариантами. Эти инварианты должны отражаться в модели (агрегатах, сущностях, VO)
🔸Агрегат сохраняется в БД как единое целое, в транзакции. Кстати, загружаются тоже целиком.
🔸Изменение состояния возможно только через вызов методов. Как и в сущностях.
🔸Агрегат может быть связан с другим агрегатом через ID.
🔸Внимание: сущность или VO не может существовать отдельно от агрегата:
1. Каждый раз добавляя сущность или VO, думаем к какому агрегату он принадлежат.
2. Если об этом не подумать, то мы не сможем гарантировать валидное состояние системы.
3. Давайте примем факт, что возможны исключения. Например, для простых, самодостаточных сущностей. Сущность сама может быть агрегатом.
🔸Внимание 2: как понять надо ли объединять два агрегата? Если они никак не связаны инвариантами - это два разных агрегата.
🔸Может ли быть несколько агрегатов внутри одного контекста? Может, но их не должно быть много. Если в рамках одного контекста нужно изменить несколько агрегатов, это делается через разные транзакции или сагу.
Агрегаты в одном контексте должны быть высоко связаны! Должны решать какую-то общую задачу.
🔸Как взаимодействуют агрегаты в одном контексте? Связь через ID, я об этом говорю выше. Через доменные события или взаимодействие происходит в сервисах.
🔸Божество :) Дада, не надо делать божественные агрегаты, лучше несколько.
Накидывайте в комментах кто что думает, где я не прав? может что-то упустил? нам нужна истина!
Повторить: https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/aggregate.md
#DDD
GitHub
practicalDDD/docs/aggregate.md at master · MaksimDzhangirov/practicalDDD
Practical DDD in Golang. Contribute to MaksimDzhangirov/practicalDDD development by creating an account on GitHub.
👍3🫡3👏2
Тактические паттерны DDD: Domain Events
Это сообщения, отражающие значимые изменения в домене (OrderPlaced, UserRegistered, MoneyTransfered).
Я уже писал, что доменные события это один из способов интеграции ограниченных контекстов. Ну и как следствие - слабая связность компонентов. Кроме этого, взаимодействие через события упрощает масштабирование системы.
Характеристики: именование в прошедшем времени, immutable, может содержать контекст (что произошло, когда, связанные данные, например ID).
И теперь самое важное - событие генерируется агрегатом:
🔸Вызов метода агрегата может возвращать событие.
🔸Или агрегат накапливает события в себе и есть метод, который возвращает все накопленные события.
Хорошо, мы получили события агрегата, что с ними делаем дальше?
🔸Используем паттерн медиатор https://github.com/mehdihadeli/Go-MediatR .
🔸Отправляем эти ивенты в брокер с использованием паттерна Outbox.
Некоторые правила:
🔸События - часть домена. Должны лежать рядом с агрегатами.
🔸Поддерживают семантическое версионирование. Если изменилась структура события, создаем OrderPlacedV2.
🔸Идемпотентность. Обработчики событий должны корректно обрабатывать повторные события.
🔸Событий не должно быть слишком много. Стараемся отражать только значимые изменения.
Ссылки:
https://habr.com/ru/companies/lamoda/articles/678932/ - Паттерн Outbox
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-event.md - Событие предметной области
#DDD
Это сообщения, отражающие значимые изменения в домене (OrderPlaced, UserRegistered, MoneyTransfered).
Я уже писал, что доменные события это один из способов интеграции ограниченных контекстов. Ну и как следствие - слабая связность компонентов. Кроме этого, взаимодействие через события упрощает масштабирование системы.
// Event базовый интерфейс для всех событий
type Event interface {
EventName() string
OccurredAt() time.Time
}
// OrderPlaced событие оформления заказа
type OrderPlaced struct {
EventID uuid.UUID
OrderID uuid.UUID
CustomerID uuid.UUID
Items []OrderItemDTO
CreatedAt time.Time
}
func (e OrderPlaced) EventName() string {
return "OrderPlaced"
}
func (e OrderPlaced) OccurredAt() time.Time {
return e.CreatedAt
}
Характеристики: именование в прошедшем времени, immutable, может содержать контекст (что произошло, когда, связанные данные, например ID).
И теперь самое важное - событие генерируется агрегатом:
🔸Вызов метода агрегата может возвращать событие.
🔸Или агрегат накапливает события в себе и есть метод, который возвращает все накопленные события.
// Агрегат из прошлого поста
type Order struct {
id OrderID
status Status
customerID CustomerID
items []OrderItem
// собираем ивенты
events []Event
}
// Place оформление заказа
func (o *Order) Place() error {
if o.status != Draft {
return errors.New("order cannot be placed")
}
o.status = Placed
// Генерация события
event := OrderPlaced{
EventID: uuid.New(),
OrderID: o.id,
CustomerID: o.customerID,
Items: o.convertItemsToDTO(),
CreatedAt: time.Now(),
}
o.events = append(o.events, event)
return nil
}
// PullEvents возвращает события и очищает список
func (o *Order) PullEvents() []Event {
events := o.events
o.events = nil
return events
}
Хорошо, мы получили события агрегата, что с ними делаем дальше?
🔸Используем паттерн медиатор https://github.com/mehdihadeli/Go-MediatR .
🔸Отправляем эти ивенты в брокер с использованием паттерна Outbox.
Некоторые правила:
🔸События - часть домена. Должны лежать рядом с агрегатами.
🔸Поддерживают семантическое версионирование. Если изменилась структура события, создаем OrderPlacedV2.
🔸Идемпотентность. Обработчики событий должны корректно обрабатывать повторные события.
🔸Событий не должно быть слишком много. Стараемся отражать только значимые изменения.
Ссылки:
https://habr.com/ru/companies/lamoda/articles/678932/ - Паттерн Outbox
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-event.md - Событие предметной области
#DDD
GitHub
GitHub - mehdihadeli/Go-MediatR: 🚃 A library for handling mediator patterns and simplified CQRS patterns within an event-driven…
🚃 A library for handling mediator patterns and simplified CQRS patterns within an event-driven architecture, inspired by csharp MediatR library. - mehdihadeli/Go-MediatR
50👍4🫡3👏2
Тактические паттерны DDD: Domain Service
Это объект, который содержит логику, не принадлежащую ни одному агрегату или сущности.
Используется, когда:
🔸Для выполнении операции нужны внешние зависимости. Например, репозитории или клиенты к сторонним API.
🔸Операция затрагивает несколько агрегатов.
🔸Операция отвечает ТОЛЬКО за бизнес-логику.
Для себя я четко разделяю Domain Service и Application Service (Usecase). Подробнее о различиях я напишу в следующем посте. А пока что, для меня Domain Service должен инкапсулировать бизнес-логику, относящуюся к домену, а также важные бизнес-знания или инварианты. Например, определение дубликата какой-либо записи: если это важно в ограниченном контексте, то эти знания относятся к Domain Service.
Пример 2. Пусть у нас есть бизнес-правило: "Покупатель может оформить заказ только в том случае, если все товары в заказе есть в наличии на складе". Для проверки этого правила необходимо работать с двумя сущностями: Product и Order. Поскольку нет никаких инфраструктурных зависимостей, это чистая доменная логика. Следовательно, нам нужен OrderService.
Некоторые характеристики:
🔸Domain Service не должны быть божественными, лучше несколько небольших, относящихся к определенным задачам классов.
🔸Сервис не должен содержать логику, относящуюся к агрегату. Например, изменение статуса.
🔸Доменный сервис не должен обращаться к данным соседнего ограниченного контекста.
🔸Сервис может изменять только 1 агрегат.
🔸Доменный сервис используется на уровне Application Service.
Ссылки:
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-service.md
#DDD
Это объект, который содержит логику, не принадлежащую ни одному агрегату или сущности.
Используется, когда:
🔸Для выполнении операции нужны внешние зависимости. Например, репозитории или клиенты к сторонним API.
🔸Операция затрагивает несколько агрегатов.
🔸Операция отвечает ТОЛЬКО за бизнес-логику.
package user
type EmailValidator struct {
userRepo UserRepository
}
func NewEmailValidator(repo UserRepository) *EmailValidator {
return &EmailValidator{userRepo: repo}
}
func (v *EmailValidator) IsEmailUnique(email string) (bool, error) {
existing, err := v.userRepo.FindByEmail(email)
if err != nil {
return false, err
}
return existing == nil, nil
}
Для себя я четко разделяю Domain Service и Application Service (Usecase). Подробнее о различиях я напишу в следующем посте. А пока что, для меня Domain Service должен инкапсулировать бизнес-логику, относящуюся к домену, а также важные бизнес-знания или инварианты. Например, определение дубликата какой-либо записи: если это важно в ограниченном контексте, то эти знания относятся к Domain Service.
Пример 2. Пусть у нас есть бизнес-правило: "Покупатель может оформить заказ только в том случае, если все товары в заказе есть в наличии на складе". Для проверки этого правила необходимо работать с двумя сущностями: Product и Order. Поскольку нет никаких инфраструктурных зависимостей, это чистая доменная логика. Следовательно, нам нужен OrderService.
Некоторые характеристики:
🔸Domain Service не должны быть божественными, лучше несколько небольших, относящихся к определенным задачам классов.
🔸Сервис не должен содержать логику, относящуюся к агрегату. Например, изменение статуса.
🔸Доменный сервис не должен обращаться к данным соседнего ограниченного контекста.
🔸Сервис может изменять только 1 агрегат.
🔸Доменный сервис используется на уровне Application Service.
Ссылки:
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-service.md
#DDD
GitHub
practicalDDD/docs/domain-service.md at master · MaksimDzhangirov/practicalDDD
Practical DDD in Golang. Contribute to MaksimDzhangirov/practicalDDD development by creating an account on GitHub.
👍4👏3🫡2
Domain Service vs Application Service vs Use Case
Кратко, это всё сервисы, разница лишь в том, на каком уровне они работают.
Доменный сервис принадлежит уровню домена, инкапсулирует важные знания о предметной области и не должен иметь сложные зависимости на инфраструктуру.
Например, я работал в телекоммуникационной компании, и вот какие методы были в одном из доменных сервисах:
🔸IsAlreadyCommunicated - проверка на то, была ли уже коммуникация по номеру телефона. Важное знание? - Конечно, как определить, что мы уже звонили по этому номеру?
🔸CanCommunicateNow - часто есть ограничение на время звонка. Этот метод проверяет, можем ли мы сейчас позвонить?
🔸GetNextCommunicationTime - если мы позвонили, а номер был занят, нужно определить время перезвона.
Конечно, может возникнуть желание положить подобные методы на уровень юзкейса или апп. сервиса, но тут значимость метода определяет его уровень.
Часто, для простоты, Application Service приравнивают к Use Case, но иногда их различают по уровню абстракции.
Итак, Application Service находится на уровне приложения и координирует работу доменных объектов и инфраструктуры (репозиториев, внешних интеграций, отправки сообщений). Он может использовать Domain Service или соседние Application Service в монолите. Не должен содержать бизнес-логику, только координацию. На этом же уровне также находится и преобразование из DTO в сущности.
Use Case - это конкретный сценарий взаимодействия пользователя с системой. Очень похож на Application Service, но фокусируется на одной бизнес-операции, не зависит от инфраструктуры. Например, CreateTask, мне очень напоминает команды в CQRS.
Разобраться, что куда оказалось сложнее, чем я ожидал. Давайте подведем итог:
🔸Domain Service - содержит доменные знания.
🔸Use Case - это бизнес-логика для конкретного сценария. Не зависит от инфраструктуры, но может зависеть от Domain Service.
🔸Application Service - это оркестрация и взаимодействие с внешним миром. Может зависеть от одного или нескольких Use Case и Domain Service.
Еще упрощаем: берем для примера метод CreateOrder. Это может быть как отдельный Use Case, так и отдельный метод в Application Service. Как по мне, так если мы будем использовать все 3 уровня, архитектура сильно усложнится.
Пусть у нас есть PostgreSQL, Kafka и какие-то интеграции. Наличие кафки говорит нам сразу про Application Service. Вот и пускай остается только Application Service.
Или у нас Event Sourcing и CQRS: консьюмер прочитал ивент и вызвал команду. А команда реализована как отдельный Use Case.
Итого: пишем код не там где удобно, а там, где правильно, включаем голову.
Кратко, это всё сервисы, разница лишь в том, на каком уровне они работают.
Доменный сервис принадлежит уровню домена, инкапсулирует важные знания о предметной области и не должен иметь сложные зависимости на инфраструктуру.
Например, я работал в телекоммуникационной компании, и вот какие методы были в одном из доменных сервисах:
🔸IsAlreadyCommunicated - проверка на то, была ли уже коммуникация по номеру телефона. Важное знание? - Конечно, как определить, что мы уже звонили по этому номеру?
🔸CanCommunicateNow - часто есть ограничение на время звонка. Этот метод проверяет, можем ли мы сейчас позвонить?
🔸GetNextCommunicationTime - если мы позвонили, а номер был занят, нужно определить время перезвона.
Конечно, может возникнуть желание положить подобные методы на уровень юзкейса или апп. сервиса, но тут значимость метода определяет его уровень.
Часто, для простоты, Application Service приравнивают к Use Case, но иногда их различают по уровню абстракции.
Итак, Application Service находится на уровне приложения и координирует работу доменных объектов и инфраструктуры (репозиториев, внешних интеграций, отправки сообщений). Он может использовать Domain Service или соседние Application Service в монолите. Не должен содержать бизнес-логику, только координацию. На этом же уровне также находится и преобразование из DTO в сущности.
Use Case - это конкретный сценарий взаимодействия пользователя с системой. Очень похож на Application Service, но фокусируется на одной бизнес-операции, не зависит от инфраструктуры. Например, CreateTask, мне очень напоминает команды в CQRS.
Разобраться, что куда оказалось сложнее, чем я ожидал. Давайте подведем итог:
🔸Domain Service - содержит доменные знания.
🔸Use Case - это бизнес-логика для конкретного сценария. Не зависит от инфраструктуры, но может зависеть от Domain Service.
🔸Application Service - это оркестрация и взаимодействие с внешним миром. Может зависеть от одного или нескольких Use Case и Domain Service.
Еще упрощаем: берем для примера метод CreateOrder. Это может быть как отдельный Use Case, так и отдельный метод в Application Service. Как по мне, так если мы будем использовать все 3 уровня, архитектура сильно усложнится.
Пусть у нас есть PostgreSQL, Kafka и какие-то интеграции. Наличие кафки говорит нам сразу про Application Service. Вот и пускай остается только Application Service.
Или у нас Event Sourcing и CQRS: консьюмер прочитал ивент и вызвал команду. А команда реализована как отдельный Use Case.
Итого: пишем код не там где удобно, а там, где правильно, включаем голову.
👍4👏2🫡2
CAP-теорема
Это теорема о компромиссах в распределенных системах, которая гласит: Невозможно обеспечить все три свойства. Система может гарантировать только два из них.
Еще раз, эта теорема работает только для распределенных систем, если же распределения нет, системы могут обеспечивать все 3 свойства.
🔸C (consistency) - согласованность. Все узлы системы видят одни и те же данные в один момент времени.
Пример: После записи новых данных любой последующий запрос вернет актуальное значение.
🔸A (availability) - доступность. Система всегда отвечает на запросы, даже если часть узлов недоступна.
Пример: При сбое одного сервера клиенты продолжают получать ответы от оставшихся узлов.
🔸P (partition tolerance) - устойчивость к распределению. Система продолжает работать, несмотря на разрывы связи между узлами (network partition).
Пример: Два дата-центра теряют связь, но каждый из них обслуживает запросы локально.
Сейчас редко встретишь БД без репликации (single-node instance). Поэтому Partition Tolerance неизбежна. Остается сделать выбор между CP или AP.
🔸CP-системы: жертвуют доступностью ради согласованности.
Для систем где важна точность: финансовые транзакции, банкинг, бронирование.
- PostgreSQL (с синхронной репликацией). Если мастер-узел теряет связь с репликой, запись данных блокируется до восстановления соединения
- MongoDB (с настройками Write Concern = majority и Read Concern = majority). При сетевом разделе MongoDB теряет доступность до завершения выборов новой первичной реплики.
- Redis (с Redis Sentinel и минимальной настройкой для записи). В случае разделения сети Redis может отказаться принимать записи, если недостаточное количество реплик доступно.
- ZooKeeper. Если кластер разделяется на две части, меньшая часть перестает отвечать на запросы. Большая часть продолжает работать, сохраняя согласованность.
🔸AP-системы: жертвуют согласованностью ради доступности.
Для масштабируемости и отказоустойчивости: соцсети, мессенджер, IoT. В системах этого типа можно сгладить отсутствие согласованности через паттерн Сага.
- Memcached. Может использовать репликацию для повышения доступности, репликация обычно является асинхронной и не гарантирует строгую согласованность между репликами. Поддерживает eventual consistency за счет TTL на данные.
- Cassandra. В случае разделения сети может вернуть устаревшие или противоречивые данные. Однако, в конечном итоге, все данные будут согласованы (eventual consistency).
- Riak. Разрешает конфликты данных с помощью CRDT (Conflict-Free Replicated Data Types).
Это теорема о компромиссах в распределенных системах, которая гласит: Невозможно обеспечить все три свойства. Система может гарантировать только два из них.
Еще раз, эта теорема работает только для распределенных систем, если же распределения нет, системы могут обеспечивать все 3 свойства.
🔸C (consistency) - согласованность. Все узлы системы видят одни и те же данные в один момент времени.
Пример: После записи новых данных любой последующий запрос вернет актуальное значение.
🔸A (availability) - доступность. Система всегда отвечает на запросы, даже если часть узлов недоступна.
Пример: При сбое одного сервера клиенты продолжают получать ответы от оставшихся узлов.
🔸P (partition tolerance) - устойчивость к распределению. Система продолжает работать, несмотря на разрывы связи между узлами (network partition).
Пример: Два дата-центра теряют связь, но каждый из них обслуживает запросы локально.
Сейчас редко встретишь БД без репликации (single-node instance). Поэтому Partition Tolerance неизбежна. Остается сделать выбор между CP или AP.
🔸CP-системы: жертвуют доступностью ради согласованности.
Для систем где важна точность: финансовые транзакции, банкинг, бронирование.
- PostgreSQL (с синхронной репликацией). Если мастер-узел теряет связь с репликой, запись данных блокируется до восстановления соединения
- MongoDB (с настройками Write Concern = majority и Read Concern = majority). При сетевом разделе MongoDB теряет доступность до завершения выборов новой первичной реплики.
- Redis (с Redis Sentinel и минимальной настройкой для записи). В случае разделения сети Redis может отказаться принимать записи, если недостаточное количество реплик доступно.
- ZooKeeper. Если кластер разделяется на две части, меньшая часть перестает отвечать на запросы. Большая часть продолжает работать, сохраняя согласованность.
🔸AP-системы: жертвуют согласованностью ради доступности.
Для масштабируемости и отказоустойчивости: соцсети, мессенджер, IoT. В системах этого типа можно сгладить отсутствие согласованности через паттерн Сага.
- Memcached. Может использовать репликацию для повышения доступности, репликация обычно является асинхронной и не гарантирует строгую согласованность между репликами. Поддерживает eventual consistency за счет TTL на данные.
- Cassandra. В случае разделения сети может вернуть устаревшие или противоречивые данные. Однако, в конечном итоге, все данные будут согласованы (eventual consistency).
- Riak. Разрешает конфликты данных с помощью CRDT (Conflict-Free Replicated Data Types).
👍3🫡3
Антипаттерны доменной модели
🔸Анемичная модель (Anemic Domain Model)
Сущность не содержит никакой бизнес-логики (методов). По сути, сущность используется как DTO.
Это влечет за собой нарушение инкапсуляции, логика уезжает на прикладной уровень.
🔸ORM-анемия
Это частный случай анемичной модели, когда ORM (таблица) диктует структуру доменного объекта.
Кроме того, это нарушает SRP. Используем DTO для маппинга агрегата в репозитории.
Важно: сначала домен, потом инфраструктура.
🔸Божественный агрегат
Агрегат начинает управлять большим количеством сущностей и содержит очень много логики.
101% нарушены границы транзакций. Исправляется разделением агрегатов на кластеры по бизнес-процессам.
🔸Божественный сервис
Модель находится в анемичном состоянии, сервис берет на себя всю логику. Смысл модели пропадает, код становится непредсказуемым.
Важно: вся логика должна быть в агрегатах, сущностях и VO.
🔸Отсутствие Value Objects
В сущностях, вместо VO используются примитивные типы.
Сильно усложняет понимание кода, увеличивается вероятность простых ошибок.
🔸Нарушение границ контекстов
Смешивание моделей из разных контекстов.
Изменения в одном контексте ломают другой.
🔸Отсутствие тестов и изоляции в доменном слое
Про тесты это больше моё правило, домен должен быть весь покрыт тестами. А также, в домене не должно быть никаких инфраструктурных зависимостей (от БД или фреймворка).
#DDD
🔸Анемичная модель (Anemic Domain Model)
Сущность не содержит никакой бизнес-логики (методов). По сути, сущность используется как DTO.
Это влечет за собой нарушение инкапсуляции, логика уезжает на прикладной уровень.
🔸ORM-анемия
Это частный случай анемичной модели, когда ORM (таблица) диктует структуру доменного объекта.
type OrderModel struct {
ID string `gorm:"primaryKey"`
Status string
}Кроме того, это нарушает SRP. Используем DTO для маппинга агрегата в репозитории.
Важно: сначала домен, потом инфраструктура.
🔸Божественный агрегат
Агрегат начинает управлять большим количеством сущностей и содержит очень много логики.
type User struct {
ID uuid.UUID
Orders []Order
Payments []Payment
Addresses []Address
}101% нарушены границы транзакций. Исправляется разделением агрегатов на кластеры по бизнес-процессам.
🔸Божественный сервис
Модель находится в анемичном состоянии, сервис берет на себя всю логику. Смысл модели пропадает, код становится непредсказуемым.
func (s *OrderService) CancelOrder(orderID uuid.UUID) error {
order, _ := s.repo.FindByID(orderID)
if order.Status == "paid" { // Валидация в сервисе
return errors.New("нельзя отменить оплаченный заказ")
}
order.Status = "cancelled"
return s.repo.Save(order)
}Важно: вся логика должна быть в агрегатах, сущностях и VO.
🔸Отсутствие Value Objects
В сущностях, вместо VO используются примитивные типы.
type OrderModel struct {
ID string
Status string
}Сильно усложняет понимание кода, увеличивается вероятность простых ошибок.
🔸Нарушение границ контекстов
Смешивание моделей из разных контекстов.
Изменения в одном контексте ломают другой.
// Контекст "Заказы" зависит от "Каталог"
type Order struct {
Product Product // Модель из другого контекста
}
// Использем ID вместо объектов
type Order struct {
ProductID uuid.UUID
}
🔸Отсутствие тестов и изоляции в доменном слое
Про тесты это больше моё правило, домен должен быть весь покрыт тестами. А также, в домене не должно быть никаких инфраструктурных зависимостей (от БД или фреймворка).
#DDD
5🫡2
Тактические паттерны DDD: Repository
Репозиторий инкапсулирует в себе логику хранения агрегата в базе данных, а так же:
- Изолирует домен от деталей инфраструктуры.
- Упрощает разработку и тестирование. Используя моки можно отложить принятие решения о БД в самый конец.
🔸Интерфейс репозитория может находиться на уровне домена, если его нужно будет использовать в доменных сервисах. С одной стороны можно замокать интерфейс в одном месте сразу со всеми методами, а с другой такой подход противоречит принципам golang: интерфейс определяется там где используется.
🔸Главная функция репозитория - работа с одним агрегатом. Включая вложенные сущности. Т.е. соблюдение транзакционной целостности.
🔸Методы репозитория принимают и возвращают агрегаты, но при этом внутри метода должны использоваться DTO для трансляции агрегатов в поля БД.
🔸Ненужно стараться использовать универсальные методы в репозитории. Метод FindActiveUsers вполне подходит.
🔸Репозиторий должен возвращать ошибки, понятные в домене. Например ErrOrderNotFound, вместо sql.ErrNoRows.
#DDD
Репозиторий инкапсулирует в себе логику хранения агрегата в базе данных, а так же:
- Изолирует домен от деталей инфраструктуры.
- Упрощает разработку и тестирование. Используя моки можно отложить принятие решения о БД в самый конец.
type OrderRepository interface {
Load(context.Contex, OrderID) (*Order, error)
Save(context.Context, Order) error
Delete(context.Context, OrderID) error
}🔸Интерфейс репозитория может находиться на уровне домена, если его нужно будет использовать в доменных сервисах. С одной стороны можно замокать интерфейс в одном месте сразу со всеми методами, а с другой такой подход противоречит принципам golang: интерфейс определяется там где используется.
🔸Главная функция репозитория - работа с одним агрегатом. Включая вложенные сущности. Т.е. соблюдение транзакционной целостности.
🔸Методы репозитория принимают и возвращают агрегаты, но при этом внутри метода должны использоваться DTO для трансляции агрегатов в поля БД.
type OrderDTO struct{}
func (o *OrderDTO) ToEntity() *model.Order {
return &model.Order{}
}
func NewRow(order model.Order) OrderDTO {
}🔸Ненужно стараться использовать универсальные методы в репозитории. Метод FindActiveUsers вполне подходит.
🔸Репозиторий должен возвращать ошибки, понятные в домене. Например ErrOrderNotFound, вместо sql.ErrNoRows.
#DDD
👍2🫡1
Паттерн Unit of Work
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.
🔸Реализация 1: в лоб, в методе апликейшен сервиса создаем транзакцию и используя ее выполняем действия в двух репах.
🔸Реализация 2: изолируем изменения в UoW.
Интерфейс UoW объекта относится к уровню приложения (application):
Пример одного из репозиториев:
И сам Unit of Work (относится к инфраструктурному слою):
Ну и пример использования в методе application service:
🔸Несколько правил:
- UoW может работать в application или domain сервисах.
- Не должен быть глобальным, божественным UoW. Лучше ограничить каким-то контекстом операции (PaymentUnitOfWork, UserUnitOfWork, OrderTransactionManager).
- Не забывайте что долгие транзакции блокируют ресурсы.
🔸Saga используется для распределенных транзакций, тогда как UoW - для локальных в рамках одной БД.
🔸Как размазать по слоям UoW в гексагональной архитектуре:
#patterns
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.
🔸Реализация 1: в лоб, в методе апликейшен сервиса создаем транзакцию и используя ее выполняем действия в двух репах.
🔸Реализация 2: изолируем изменения в UoW.
Интерфейс UoW объекта относится к уровню приложения (application):
type CreateOrderUnitOfWork interface {
Begin(ctx context.Context) error
Commit() error
Rollback() error
// Репозитории, управляемые UoW
OrderRepository() OrderRepository
ProductRepository() ProductRepository
}Пример одного из репозиториев:
type OrderRepository struct {
tx *sql.Tx
}
func NewOrderRepository(tx *sql.Tx) *OrderRepository {
return &OrderRepository{
tx: tx,
}
}
func (r *OrderRepository) Save(ctx context.Context, order *Order) error {
_, err := r.tx.ExecContext(ctx, "INSERT INTO orders (id, customer_id, total_amount) VALUES (?, ?, ?)", order.ID, order.CustomerID, order.TotalAmount)
return err
}И сам Unit of Work (относится к инфраструктурному слою):
type CreateOrderUnitOfWork struct {
db *sql.DB
tx *sql.Tx
orderRepository OrderRepository
productRepository ProductRepository
}
func NewCreateOrderUnitOfWork(db *sql.DB) *CreateOrderUnitOfWork {
return &CreateOrderUnitOfWork{
db: db,
}
}
func (u *CreateOrderUnitOfWork) Begin(ctx context.Context) error {
tx, err := u.db.BeginTx(ctx, nil)
if err != nil {
return err
}
u.tx = tx
u.orderRepository = NewOrderRepository(tx)
u.productRepository = NewProductRepository(tx)
return nil
}
func (u *CreateOrderUnitOfWork) Commit() error {
return u.tx.Commit()
}
func (u *CreateOrderUnitOfWork) Rollback() error {
return u.tx.Rollback()
}
func (u *CreateOrderUnitOfWork) OrderRepository() OrderRepository {
return u.orderRepository
}
func (u *CreateOrderUnitOfWork) ProductRepository() ProductRepository {
return u.productRepository
}Ну и пример использования в методе application service:
type OrderService struct {
createOrder CreateOrderUnitOfWork
}
func (s *OrderService) CreateOrder(ctx context.Context, productID domain.ProductID) error {
if err := s.createOrder.Begin(ctx); err != nil {
return err
}
defer s.createOrder.Rollback()
product, err := s.createOrder.ProductRepository().FindByID(ctx, productID)
if err != nil {
return err
}
order := domain.NewOrder()
order.AddItem(product)
if err := s.createOrder.OrderRepository().Save(ctx, order); err != nil {
return err
}
return s.createOrder.Commit()
}🔸Несколько правил:
- UoW может работать в application или domain сервисах.
- Не должен быть глобальным, божественным UoW. Лучше ограничить каким-то контекстом операции (PaymentUnitOfWork, UserUnitOfWork, OrderTransactionManager).
- Не забывайте что долгие транзакции блокируют ресурсы.
🔸Saga используется для распределенных транзакций, тогда как UoW - для локальных в рамках одной БД.
🔸Как размазать по слоям UoW в гексагональной архитектуре:
├── domain/ # Доменный слой
│ ├── order.go # Агрегат Order
│ └── product.go # Агрегат Product
│
├── application/ # Прикладной слой
│ ├── ports/ # Порты (интерфейсы)
│ │ └── uow.go # Интерфейс UnitOfWork
│ └── services/ # Сервисы приложения
│ └── order_service.go # Use Case: оформление заказа
│
└── adapters/ # Инфраструктурный слой
└── persistence/
├── create_order_uow.go # Реализация CreateOrderUnitOfWork
└── postgre/ # Реализации репозиториев
└── order_repository.go
#patterns
👍5🫡2
Паттерн Strategy
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS или посредством звонка.
А сам выбор типа коммуникации зависит от каких-то настроек (приложения или конкретного пользователя).
То есть, возможны две стратегии коммуникации: SMS или CRM. Так вот, паттерн стратегия предлагает изолировать конкретные стратегии в отдельные классы (структуры), а выше них ставить класс, который будет ими управлять.
Как мы уже знаем, проектирование всегда идет от модели данных. В нашем случае это набор данных, по которым мы можем понять, кому, как и что нужно отправить.
Далее определяем интерфейс, который будет реализован стратегиями:
Реализуем этот интерфейс:
Ну и сам менеджер стратегий:
Таким образом, менеджер содержит набор всех возможных стратегий коммуникации. Их не обязательно задавать в фабричном методе - можно и передавать. Используя данные из модели, определяем стратегию и отправляем сообщение.
Взглянем немного шире: таким образом, мы поддержали Open-Close Principle.
Конечно сам выбор стратегии может быть сложнее - тогда мы просто изолируем выбор стратегии в отдельный метод менеджера.
Дальше будет еще несколько постов про самые важные, на мой взгляд, паттерны.
#patterns
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS или посредством звонка.
А сам выбор типа коммуникации зависит от каких-то настроек (приложения или конкретного пользователя).
То есть, возможны две стратегии коммуникации: SMS или CRM. Так вот, паттерн стратегия предлагает изолировать конкретные стратегии в отдельные классы (структуры), а выше них ставить класс, который будет ими управлять.
Как мы уже знаем, проектирование всегда идет от модели данных. В нашем случае это набор данных, по которым мы можем понять, кому, как и что нужно отправить.
type CommunicationStrategy string
const (
SMS CommunicationStrategy = "SMS"
AmoCRM CommunicationStrategy = "AmoCRM"
)
type Message struct {
From string
To string
Text string
CommunicateVIA CommunicationStrategy
}
Далее определяем интерфейс, который будет реализован стратегиями:
type Communitator interface {
SendMessage(context.Context, Message) error
}Реализуем этот интерфейс:
type SMS struct {
}
func (c *SMS) SendMessage(ctx context.Context, msg Message) error {
// подключение к SMS-сервису
// отправка SMS
return nil
}
type AmoCRM struct {
}
func (c *AmoCRM) SendMessage(ctx context.Context, msg Message) error {
// работа с API
return nil
}Ну и сам менеджер стратегий:
type CommunicationManager struct {
strategies map[CommunicationStrategy]Communitator
}
func NewCommunicationManager() *CommunicationManager {
return &CommunicationManager{
strategies: map[CommunicationStrategy]Communitator{
SMS: &SMS{},
AmoCRM: &AmoCRM{},
},
}
}
func (m *CommunicationManager) SendMessage(ctx context.Context, msg Message) error {
if communitator, ok := m.strategies[msg.CommunicateVIA]; ok {
return communitator.SendMessage(ctx, msg)
}
return fmt.Errorf("unsupported communication strategy: %s", msg.CommunicateVIA)
}Таким образом, менеджер содержит набор всех возможных стратегий коммуникации. Их не обязательно задавать в фабричном методе - можно и передавать. Используя данные из модели, определяем стратегию и отправляем сообщение.
Взглянем немного шире: таким образом, мы поддержали Open-Close Principle.
Конечно сам выбор стратегии может быть сложнее - тогда мы просто изолируем выбор стратегии в отдельный метод менеджера.
Дальше будет еще несколько постов про самые важные, на мой взгляд, паттерны.
#patterns
👏5👍2🫡2
Паттерн Specification
Этот паттерн помогает выразить сложные бизнес-правила (критерии) через отдельные, переиспользуемые объекты.
Пример правил:
- Пользователь должен быть старше 18 лет.
- Пользователь должен быть активным.
- Пользователь должен иметь роль покупателя.
Решение в лоб: наделать кучу ифов.
Решение по уму: паттерн Specification, где
- Выносим каждое правило в отдельный объект.
- Легко тестировать.
- Легко комбинировать друг с другом.
Для начала определим интерфейс проверки правил:
Добавим пару спецификаций:
И их комбинацию:
OR-спецификация выглядит так:
Проверяем работу:
Когда использовать Specification:
- Выражение сложных бизнес-правил в коде.
- Когда бизнес-правила часто меняются.
- Когда нужно переиспользовать правила в разный частях приложения.
#patterns
Этот паттерн помогает выразить сложные бизнес-правила (критерии) через отдельные, переиспользуемые объекты.
Пример правил:
- Пользователь должен быть старше 18 лет.
- Пользователь должен быть активным.
- Пользователь должен иметь роль покупателя.
type User struct {
Age int
IsActive bool
}Решение в лоб: наделать кучу ифов.
Решение по уму: паттерн Specification, где
- Выносим каждое правило в отдельный объект.
- Легко тестировать.
- Легко комбинировать друг с другом.
Для начала определим интерфейс проверки правил:
type Specification[T any] interface {
IsValid(candidate T) bool
}Добавим пару спецификаций:
type AgeSpecification struct {
minAge int
}
func NewAgeSpecification(minAge int) *AgeSpecification {
return &AgeSpecification{minAge: minAge}
}
func (s *AgeSpecification) IsValid(candidate User) bool {
return candidate.Age >= s.minAge
}
type ActiveSpecification struct{}
func NewActiveSpecification() *ActiveSpecification {
return &ActiveSpecification{}
}
func (s *ActiveSpecification) IsValid(candidate User) bool {
return candidate.IsActive
}И их комбинацию:
type AndSpecification[T any] struct {
specifications []Specification[T]
}
func NewAndSpecification[T any](specifications ...Specification[T]) *AndSpecification[T] {
return &AndSpecification[T]{specifications: specifications}
}
func (s *AndSpecification[T]) IsValid(candidate T) bool {
for _, specification := range s.specifications {
if !specification.IsValid(candidate) {
return false
}
}
return true
}OR-спецификация выглядит так:
func (s *OrSpecification) IsValid(candidate T) bool {
for _, specification := range s.specifications {
if specification.IsValid(product) {
return true
}
}
return false
}Проверяем работу:
func main() {
user := User{Age: 20, IsActive: false}
andSpec := NewAndSpecification(
NewAgeSpecification(18),
NewActiveSpecification(),
)
fmt.Println(andSpec.IsValid(user))
}Когда использовать Specification:
- Выражение сложных бизнес-правил в коде.
- Когда бизнес-правила часто меняются.
- Когда нужно переиспользовать правила в разный частях приложения.
#patterns
👍4🫡2
Паттерн Outbox
Паттерн обеспечивает атомарность при работе с БД и брокерами сообщений.
Пример 1: Идет транзакция изменения агрегата. И на каком-то шаге нужно отправить ивент в Kafka, отправляем ивент, а на следующем шаге транзакция падает с ошибкой. Но ивент уже отправлен.
Пример 2: Консюмер читает ивенты из Kafka и выполняет действия. Вопрос: как обеспечить идемпотентность?
Основная идея:
🔸Сохраняем событие в таблицу outbox в рамках той же транзакции, что и изменение данных агрегата.
🔸Фоновым процессом читаем эту таблицу и отправляем события в брокер.
🔸После успешной отправки или меняем статус события, или удаляем из таблицы.
Пример таблицы:
Далее добавляем сущность OrderEvent и репозиторий ее сохранения.
Фоновый процесс:
В простом случае - удаляем, но можем и менять статус. А также можно считать количество попыток, добавить метрики, подумать про лочки.
Как обработать событие со стороны консюмера?
Точно также: сначала кладем в БД, потом фоновым процессом вычитываем и обрабатываем.
Обратите внимание на EventID, это наша идемпотентность.
Итого
Паттерн отлично подходит для агрегатов и доменных событий. Хорошо ложится в CQRS и Unit of Work.
#patterns #DDD
Паттерн обеспечивает атомарность при работе с БД и брокерами сообщений.
Пример 1: Идет транзакция изменения агрегата. И на каком-то шаге нужно отправить ивент в Kafka, отправляем ивент, а на следующем шаге транзакция падает с ошибкой. Но ивент уже отправлен.
Пример 2: Консюмер читает ивенты из Kafka и выполняет действия. Вопрос: как обеспечить идемпотентность?
Основная идея:
🔸Сохраняем событие в таблицу outbox в рамках той же транзакции, что и изменение данных агрегата.
🔸Фоновым процессом читаем эту таблицу и отправляем события в брокер.
🔸После успешной отправки или меняем статус события, или удаляем из таблицы.
Пример таблицы:
CREATE TABLE outbox (
id UUID PRIMARY KEY,
event_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Далее добавляем сущность OrderEvent и репозиторий ее сохранения.
Фоновый процесс:
func (w *OutboxWorker) Start(ctx context.Context) {
ticker := time.NewTicker(w.pollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
events, _ := w.outboxRepo.FetchPending(ctx, 100)
for _, event := range events {
if err := w.producer.Send(event); err != nil {
continue
}
_ = w.outboxRepo.Delete(ctx, event.ID)
}
case <-ctx.Done():
return
}
}
}В простом случае - удаляем, но можем и менять статус. А также можно считать количество попыток, добавить метрики, подумать про лочки.
Как обработать событие со стороны консюмера?
Точно также: сначала кладем в БД, потом фоновым процессом вычитываем и обрабатываем.
Обратите внимание на EventID, это наша идемпотентность.
Итого
Паттерн отлично подходит для агрегатов и доменных событий. Хорошо ложится в CQRS и Unit of Work.
#patterns #DDD
👍5👏3🫡3