Денис Пешехонов, Александр Химушкин — Укрощаем DDD на практике

Денис Пешехонов, Александр Химушкин — Укрощаем DDD на практике56:20

Информация о загрузке и деталях видео Денис Пешехонов, Александр Химушкин — Укрощаем DDD на практике

Автор:

DotNext — конференция для .NET‑разработчиков

Дата публикации:

21.06.2024

Просмотров:

4.1K

Транскрибация видео

Спикер 3

Друзья, всем привет!

Пожалуйста, соберитесь, мобилизуйтесь, потому что нас с вами ждет очень сложная, но важная тема Domain Driven Design.

И сегодня нам ее представят тандем из архитектора и темлида.

Пожалуйста, встречайте наших спикеров.

Александр Химушкин, Ненис Пешехонов.

Спасибо.

Спикер 1

Спасибо.

Спикер 3

Пожалуйста, ребята, вам слово.

Хорошего доклада.

Спикер 4

Спасибо большое.

Всем привет.

Меня зовут Денис, мой коллега Александр.

Мы представляем «Атомстройэкспорт».

Это часть дивизиона «Росатома», которая занимается строительством атомных станций, в основном за рубежом.

Ну и конкретно мы — это отдел цифровизации.

Саша, вот ты когда к нам пришел на работу, ты думал, что мы будем делать?

Спикер 5

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

Потом я подумал, что-то не похоже на правду.

Скорее всего, это оператор на атомной станции, что-то там за чем-то следит, смотрит, рычажки переключает, может, мониторит что-то.

Спикер 4

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

Но на самом деле нет.

У нас цифровой документооборот.

Вообще атомная стройка – это очень сложный процесс, который длится 10-20 лет.

Участвуют в этом 10 тысяч человек.

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

И, наверное, их оттуда не отпускают, пока они не построят.

Ну, в общем, мы занимаемся цифровизацией их взаимодействия.

То есть это системы электронного документооборота, системы обмена сообщениями.

Они все локально на площадках разворачиваются.

Там, как правило, нельзя брать никакие внешние решения.

И у нашей предметной области есть особенности.

Во-первых, она сложная и ни на что не похожа.

Это бухгалтерия, сметное дело, электронный документооборот, но при этом в атомной отрасли.

Куча нюансов, куча каких-то законов.

Есть там законы России, законы той страны, где строится станция.

Все это нужно вместе как-то скомпоновать.

Сложная предметная область с кучей зависимостей.

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

И вот эти два требования, они нас в какой-то момент вынудили задуматься об этом, об архитектуре.

Вот мы пригласили архитектора.

Спикер 5

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

Команды в одном микросервисе, команды в другом.

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

Для меня это был факт.

Я об этом просто столкнулся.

Зачем вам архитектор?

Я не понял этой роли для начала.

Спикер 4

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

Они делают по-своему, как хотят.

Они хорошо знают свой микросервис.

И, в общем-то, все в порядке.

К сожалению, это получилось.

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

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

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

Спикер 5

Ну да, действительно.

Значит, смотрите, как бы как решение вообще нашей проблемы, это непонятный язык, никто ничего не понимает, это все плохо, естественно.

Есть решение

Это domain-driven design.

Книг очень много на самом деле.

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

Собственно, при необходимости можете почитать ее.

Она довольно объемная, довольно интересная, но не только для программистов в первую очередь.

В этой книге мы используем несколько терминов.

В основном, самое главное – это общий язык.

Потом там есть сущности, объекты значения, репозитории, спецификации, политики и тому подобное.

Я здесь не переживайте про DDD, молчу, расскажу, потому что там основная тема немножко другой части.

Но на этом слайде я покажу, что такое общий язык.

Общий язык – это не то, что русский, английский или ещё что-то.

Это какой-то конкретный терминологический язык, который принят в каком-то конкретном проекте среди каких-то конкретных людей, задействованных на этом проекте.

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

Помимо всего прочего, общий язык преследует такую цель, как...

Документирование.

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

У нас есть...

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

Дальше общий язык должен развиваться.

Это прям обязательное требование от меня, в принципе, и на протяжении книги Эрика Эванса.

Он постоянно меняется, перерабатывается.

Если мы что-то перестали иначе понимать или что-то

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

Это как бы про общий язык.

Спикер 4

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

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

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

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

Но главное, чтобы когда они это называют, они это называли одинаково, и они говорили бы об одном и том же, и чтобы это одно и то же одинаково работало.

Спикер 5

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

Давайте дальше я расскажу про все остальные вещи, которые здесь на слайде, только по чуть-чуть.

У нас как бы на очереди следующее – это сущность.

Сущность – это…

Прямо на языке программирования мы описываем какой-то физический, не физический объект нашего реального мира в основном, ну или общего языка, который мы используем на проектах.

Соответственно, у сущности есть определенный...

Такие ограничения.

То есть она не позволяет себя инстанцировать в коде неправильно.

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

Это один момент.

Второй момент.

У сущности есть такое свойство.

Она на протяжении своего существования меняется.

Что это значит?

То есть она может быть открыта, это состояние сохраняется физически в базе, например.

Она может быть закрыта, её могут повернуть, перевернуть.

То есть вот это является изменением.

И мы, собственно, вот эту всю группу действий, которые мы можем совершать это объектом над сущностью, мы объединяем в одно слово «сущность».

Спикер 4

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

Но как только у нас появляется какой-то предмет из реального мира, где есть несколько свойств, то мы для этого создаем класс, и это автоматически превращается в сущность, получается.

Спикер 5

На самом деле нет.

Я так понимаю, ты имеешь в виду вот этот импринт – это не сущность.

Это следующий этап, скажем так, нашего повествования.

Это объект значения.

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

Но в целом это то, что в книге содержится, как аннотация книги.

Тираж книги, кто там автор, какой-нибудь СБ, написание там еще дополнительное.

Все вот это – это какая-то группа вещей, которая объединяется по какому-то определенному смыслу.

Это все должно проистекать из языка общения.

И главное отличие от сущности – оно никогда не меняется.

Оно как сохранилось, оно так и лежит в базе.

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

Хорошо.

Поэтому давайте, наверное, дальше перейдем.

Есть еще, мы используем репозиторию.

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

То есть это некая абстракция над базой данных.

Тут, наверное, ничего объяснять особо не нужно.

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

Это политики и спецификации.

Начну я со второго, спецификацию.

Это такое...

скажем так, пулевое выражение.

Здесь это как экспрессион, но у нас на проекте это немножко посложнее, немножко иначе.

Но тут я буду объяснять в основном концепцию, для чего это нужно.

Спецификация, она инкапсулирует в себе какой-то бизнес-термин, который употребляется, что вот, допустим, представим себе Вселенную, где среди множества книг единственная книга –

«Чудовище Франкенштейна», это книга-хоррор.

И когда мы в едином языке говорим, что вот книжка-то хоррор, то значит мы все понимаем, что в названии содержится Франкенштейн.

И как бы в терминах нашего бизнеса это хоррор.

Есть хоррор-бук.

Или, допустим, есть такой человек, пользователь нашей системы, которому младше 12 лет.

Это «Not 12 years old».

Соответственно, вот эта спецификация, они описывают какие-то простенькие конкретные проверочки.

А политики, политики на самом деле там очень много чего, но мы в основном используем их для следующего.

Есть, извиняюсь,

такие бизнес-термины, которые сложно описать, допустим, спецификациями.

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

тем, кто младше 12 лет, не то что книги нельзя читать, нельзя читать хорроры.

Соответственно, эта политика объединяет в себе эти две спецификации.

И если будем смотреть на эти спецификации по отдельности, то мы не поймем, ну типа хоррор, младше 12 лет, что это значит, непонятно.

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

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

То есть условия, проверяющие два критерия, что книжка не хоррор и

Типа пользователь младше 12 лет.

Спикер 4

Слушай, а почему у тебя здесь спецификации?

Это выражения, а не просто методы, которые возвращают Boolean?

Спикер 5

Как я упомянул раньше, это не совсем выражение, но это именно для концепции.

В первую очередь для того, чтобы инкапсулировать проверки.

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

Это раз.

Второе.

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

SQL.

Ну, это классика, поэтому мы их, собственно, так и используем.

Если мы объединяем несколько спецификаций, мы можем фильтровать по огромному списку.

Спикер 4

Хорошо.

Спикер 5

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

Это непонятки.

Все друг друга понимают, все знают, что такое сущности, все знают, что такое конкретные сущности, все понимают, как они себя ведут.

Ну, объекты значения, всё такое прилагается к ним.

Спикер 4

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

Мы просто усложнили модели на ровном месте, с ними сложно теперь работать, кажется, стало только хуже.

Спикер 5

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

Значит, я скажу следующее, что наш домен, нашу бизнес-логику нужно разрабатывать довольно сильным разработчиком, middle-plus, senior, архитектор, темалиты.

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

Спикер 4

Ну хорошо, но так же не может быть.

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

Что делают такие разработчики?

Чем они так занимаются?

Спикер 5

они должны разрабатывать что-то иное.

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

Спикер 4

Ну вот, я знаю, о чем Саша говорит.

Вообще, когда говорят о DDD, всегда рядом с этим упоминают так называемую луковую архитектуру, ну или clean architecture.

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

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

Идея в том, что в центре лежит домен.

У нас вот он исторически называется BLL, Business Logic Layer, поэтому иногда будем ссылаться на это.

В домене хранятся сущности, объекты значения, о которых Саша говорил, и он самодостаточен, он ни от чего не зависит.

У него вообще не должно быть никаких внешних зависимостей.

Ну, в смысле, какие-то библиотеки там, ньюгеты могут быть, но вот внутри вашей сборки у него других зависимостей нет именно от этой сборки.

И он полностью определяет законы своего существования.

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

Вокруг него есть слой приложения.

Чуть попозже объясню, чем он отличается.

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

И вокруг этого всего есть слой инфраструктуры.

Он знает о слое приложения, знает о слое домена, но в обратную сторону зависимости нет.

Приложение не знает о слое инфраструктуры, домен не знает о слое инфраструктуры и так далее.

Как я уже сказал, домен определяет полностью законы жизни сущности.

Это как бы вселенная, в которой мы существуем.

Законы физики.

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

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

Во-первых, он защищает от ошибок.

Он бьет разработчика по рукам, если разработчик пытается сделать что-то не то.

Например, чтобы открыть книгу, ее можно открыть только на какой-то странице.

Нельзя ни на какой странице открыть книгу.

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

Ну и во-вторых, там куча проверок внутри, исключение времени выполнения.

Если вы пытаетесь сделать что-то не то, допустим, открыть книгу на странице, которой в книге не существует номер,

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

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

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

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

И я, если бы не работал никогда с этой предметной областью, с книгами, я не знал об этом.

А вот если бы я читал код...

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

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

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

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

И я как человек, незнакомый вообще с книжным делом, я читаю этот код и понимаю, что

Объекты реального мира, которые мы моделируем, они работают каким-то определенным образом.

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

Соответственно, разработчик просто узнает лучший предмет на область, как она работает.

Спикер 5

Слушай, Денис, ты замечательно рассказал.

Я вот тут заметил небольшую такую ошибочку.

Смотри, ISBN – это строка.

Откуда я должен знать, как она формируется?

Откуда я это узнаю?

Я могу в строчку передать всякую ерунду.

«Привет, мир», «Здравствуйте, я ваша тетя», еще что-то.

И как вообще… Подожди, как это вообще возможно?

Спикер 4

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

Вообще эта проблема называется primitive obsession, если ты слишком много используешь примитивные типы там, где тебе нужны специальные типы.

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

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

И ты, когда создаешь этот тип, можешь там какие-то валидации дополнительные добавить.

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

Вообще у нас для идентификаторов всех сущностей обязательно специальные типы, хотя идентификаторы — это, как правило, одна строка или одно число.

Еще для каких-то вещей обязательно в DDD создаются объекты значений.

Спикер 5

Да, действительно, это правильное решение.

То есть вот этот вот примитивный тип СБН, он, собственно, также отвечает, естественно, за валидацию самого себя, то есть за правильное формирование.

Но я тут заметил еще одну дополнительную ошибку.

Это как бы и строка, и класс.

Ну, все знают, что у них какие-то такие специфические поведения в разных ситуациях.

Нет ли какой-то вообще проблемы при замене такой?

Спикер 4

Ну да, я думаю, аудитория тоже заметила.

Конечно, здесь должен быть рекорд.

Вообще, до того, как рекорды появились в C-Sharp, нам приходилось писать специальные классы, которые переопределяли оператор равенства, переопределяли методы equals, и вообще постараться сделать так, чтобы они были иммутабельными, вели себя так, как значимый тип.

Но когда появились рекорды, это прям идеальное использование для объектов значений.

Они неизменяемые, они ведут себя при сравнении, просто сравнивают все по...

атрибутам своим.

В общем, прекрасно работают как раз для этого.

Поэтому да, на самом деле это рекорды.

Слой application.

Чем он отличается от слоя домен?

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

А в application отделяются специальные бизнес-процессы, которые написаны, что мы реально будем делать с этими объектами.

Как правило, тоже из литературы выделяют такое понятие, как application service.

Это класс, который инкапсулирует в себе один конкретный бизнес-процесс.

Одно действие пользователя – это один application service.

И, как правило, это одна транзакция в базе данных.

Ну и, соответственно, не обязательно этот бизнес-процесс работает с одной сущностью, он может работать с несколькими сущностями.

Допустим, вот такой немножко синтетический пример.

Чтение человеком книги — это изменение состояния памяти человека, то есть ему в память добавляется страница книги, которую он прочитал, ну и книгу перед этим надо открыть, чтобы из неё там...

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

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

Вот это бизнес-процесс, и его нужно инкапсулировать в этой application-логике.

При этом у книги может быть гораздо больше возможностей.

Ну, скажем, законы физики позволяют нам вырвать из книги страницу...

И такой метод вполне может быть у сущности книга.

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

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

Ну, я бы вообще так лихо рассказал, но есть еще проблемы.

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

Нельзя ли с этим что-то сделать?

Спикер 5

Действительно, это такая проблема, с которой мы часто очень встречаемся.

И, собственно, какое решение мы нашли для себя?

Есть такой классический паттерн.

Он давно, наверное, всем известен.

Это паттерн-декоратор.

То есть...

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

Здесь довольно все просто.

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

Внезапно у нас такая вселенная в нашем едином языке.

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

Можно сделать несколько декораторов.

Декоратор бизнес-сценария декорирует, декоратор, который декорирует.

декоратор, то есть такая матрёшка.

То есть для чего это у нас может делаться?

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

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

На проекте мы пользуемся такой библиотекой.

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

Тут всего дополнительная строчка, и поэтому при необходимости

Имейте это в виду.

Спикер 4

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

Кто смотрел доклад про workflow-архитектуру, вот что-то одноленно подобное.

Можно этажерки из этих декораторов точно так же строить.

Инфраструктура.

Спикер 5

Инфраструктура.

Давай вот, Денис, я тебе встряну, назовем так.

Я когда читал нашего дядюшку Боба или дедушку Боба, для кого как, я, собственно, не понимал вообще, что такое домен, что такое бизнес-слой, я не понимал про аппликационство, вообще ничего.

И еще больше всего я не понимал, как они...

Вроде бы зависят от инфраструктуры, не зависят от неё.

Они ей пользуются, они пишут, они читают, они ещё что-то делают.

И всё это вроде бы в аппликейшн-услове, вроде бы это бизнес-логика.

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

Спикер 4

Ну, раз мы моделируем объекты реального мира, давайте из реального мира приведу вам пример.

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

Сущности, с которыми они работают, они одинаковые.

Книга что в магазине, книга что в библиотеке книга.

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

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

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

Но нет же такого, что если там стоит полка, мы ее используем, а если не стоит, мы на пол книги сбрасываем, правильно?

Спикер 5

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

Спикер 4

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

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

Библиотека строится так, чтобы в ней были столы, какой-нибудь регистр карточек учета и что-нибудь такое.

Соответственно, идея в том, что в application слое объявляются интерфейсы с методами, которые реально нужны бизнес-сценариям.

И инфраструктура обязана это реализовать.

Пусть как хочет реализовывает.

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

В библиотеке мы могли бы объявить какой-нибудь метод проверки доступности книги на нужные даты.

И инфраструктура вынуждена подстраиваться под требования приложения, а не наоборот.

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

Нет, application задает, что ему нужно, интерфейсы объявлены в слое application, инфраструктура, поскольку она зависит от application, она эти интерфейсы в себя принимает и реализует именно так, как нужно это слое приложения.

Это своеобразная модификация принципа инверсии управления.

Спикер 5

Ну да, все как в реальной жизни.

Бизнес к нам приходит, говорит, ребята, вы делаете то или то, а мы, программисты, как инфраструктура, выполняем ее требования.

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

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

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

На самом деле давайте так.

Мы все прекрасно понимаем, что в нашей физической книге нет такого списка.

списка людей, кто её брал вообще в руки.

И это неправильно.

Спикер 4

Хорошо.

Давай создадим новую сущность, назовём её «Книга с пользователями» и добавим там отдельное поле «Книга» и отдельное поле «Пользователи».

Спикер 5

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

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

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

Спикер 4

Ну ладно, я придумал, как мне работать.

Давай фронту дадим это.

Пусть он отдельно вызывает разные методы у нас в системе и там собирает где-нибудь на своей страничке.

Рядом покажет просто количество пользователей и книжку.

Спикер 5

Ну, конечно, это вот всегда так.

И фронтенд – это наш последний бастион.

Типа, ребята, мы на бэке не хотим работать, сделайте за нас работу.

Нет, так нельзя.

Во-первых, у нас помимо того, что мы просто смотрим в интерфейсе информацию о книге и о пользователях, есть еще в интерфейсе список книг.

И иногда нам надо искать по пользователям, искать по фамилии пользователя или, например…

фильтровать.

А если еще у нас добавляется локализация, ну это же вообще какой-то кошмар.

Спикер 4

Ну сдаюсь.

Что делать?

Спикер 5

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

Мы как бы общались с нашим бизнесом постоянно, постоянно спорили, ругались.

То есть вот мы не хотим этого делать, а они хотят.

Ввели такой термин, он стал, в принципе, в нашем языке общения возникать, связь.

Мы хотим связать книгу,

и пользователей, людей.

Мы хотим связать книгу и библиотекаря.

Мы хотим там что-то связать.

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

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

И наоборот.

Мы добавили такую дополнительную сущность, это вот

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

То есть есть какая-то таблица связи, которая связывает остальные таблицы.

С другой стороны, он у нас появился в бизнес-языке.

И тут это обычно действительно сущность дополнительная.

У нее есть набор

Атрибутов, набор свойств, то есть from entity, это идентификатор книги, допустим, to entity, это идентификатор пользователя, который ее сейчас читает.

И link type, то есть это какая-то семантическая нам подсказка программистам, для чего эта связь.

Эта связь, она может меняться со временем.

То есть каким образом?

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

Я тип связи поменял.

Вот это как бы отображение, изменение сущности.

И я думаю, вроде бы проблема связи между сущностями решена.

Спикер 4

Но как все-таки нам в списке вывести сортировку по количеству пользователей, которые эту книгу читают или взяли?

Спикер 5

Ну, все просто.

Вот тут был недавний доклад про индекс.

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

Спикер 4

Ну, как-то сложно звучит.

А если у нас там локализация еще добавится какая-нибудь?

Спикер 5

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

Спикер 4

Наверное, можно сделать попроще.

Вообще со временем становится понятно, что то, что пользователь хочет видеть, и то, что хранится в сущности с точки зрения бизнес-процессов, это не всегда одно и то же.

Пользователь может хотеть видеть какое-то свойство, которого у сущности в принципе нету.

в реальном мире физически.

Ну и, соответственно, процесс чтения данных и процесс изменения данных и манипулирования бизнес-сущностями — это чуть-чуть разные процессы.

И мы поняли, что для этого нам нужно использовать такой подход, который называется CQRS — Command Query Responsibility Segregation.

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

Команда манипулирует теми самыми entity, они читаются из репозиториев, обычно одна команда это одна транзакция и команда данные меняет.

А запрос – это просто запрос на чтение данных.

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

в которой хранится источник истины.

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

Они могут быть базой данных принципиально другого типа.

Например, мы в SQL храним наши бизнес-сущности, там все обвешано проверками со всех сторон связи и так далее.

А Query Source – это какое-то денормализованное хранилище, это может быть документно-ориентированная база данных, это может быть Elasticsearch какой-нибудь для полнотекстового поиска.

И в нем хранятся данные.

Не наши бизнес-модели, а в нем хранятся специальные материализованные представления.

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

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

Эти представления описывают ровно то, что человек хочет видеть, ровно то, что нам нужно фильтровать, сортировать, выводить в каких-то списках.

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

Ну и здесь можно обойтись более примитивными типами.

Мы собираем вот это представление, оно какое-то древовидное, может лежать в «Монге», может лежать в «JSONB»,

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

То есть то, что пользователь видит, и то, с чем он реально работает, это не одно и то же.

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

Спикер 5

Ну, слушай, хорошее решение, тоже классический подход.

Денормализованная таблица специально для быстрого поиска, если необходима фильтрация.

Но вот вопрос, как это наполнять данными?

Как ты вообще предлагаешь это делать?

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

Спикер 4

Дай-ка подумаю.

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

Ну, ладно, как бы ты предложил?

Спикер 5

Ну, вот я бы предложил следующее.

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

Немножечко такой экскурс.

Значит, смотрите, у нас что-то в слое домена в Business Logic Layer происходит.

Какие-то изменения.

В application-слое эти изменения вызываются в определенном порядке.

Все такое прочее.

Все замечательно.

И со временем мы накапливаем эти изменения.

Эти доменные события могут быть пустыми.

То есть тут никаких значений может не быть.

А могут содержать какую-то информацию.

Здесь пример двух доменных событий.

Книжка была создана.

Она появилась в библиотеке каким-то образом.

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

Или она была как-то открыта, и нам не важно, какой пользователь ее открыл.

Все это у нас наполняется в таком списке доменных событий, которые объявлены в каком-то абстрактном классе сущностей.

Все остальные наши сущности его не станцилируют, а наследуют.

Спикер 4

Погоди, я вот вижу, что у тебя тут обезличенный такой iDomainEvent.

Но хорошо, я наполнил в сущности список доменных событий.

Где-то в инфраструктуре я начинаю этот список читать.

Мне, получается, нужно про каждый элемент вот этого списка сделать такую огромную... Допустим, у меня 20 доменных событий или 50 доменных событий.

Я делаю такую гигантскую этажерку.

If...

там item из такой-то тип сделать это, if item из такой-то тип сделать это.

И там такой гигантский pattern matching или что-то такое получается.

Выглядит супер неказисто.

Спикер 5

Ну, неказисто, но зато надёжно.

Но тем не менее я понял, в чём проблема.

Нет, нам так не надо.

Мы опять возвращаемся, давайте вернёмся к истокам.

Есть тоже классический паттерн visitor, visitor, visitor, кто как хочет называет.

Мы, смотрите, как бы раньше исповедовали такое, что в нашем доменном слое никаких технических трюков для того, чтобы он как-то корректно работал, не должно быть.

Потому что иначе мы там что-то нарушим, что-то не так, ослабим наши, так сказать, ограничения.

И мы сказали, вот у нас есть интерфейс IDomainEvent, у него есть метод accept, он принимает какой-то маркерный интерфейс визитера,

И, соответственно, есть еще дополнительный интерфейс, дженерик, который наследует этот маркерный интерфейс.

У него визит есть.

Ну, все вроде бы понятно.

То есть это примерно, вот можно видеть реализацию.

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

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

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

Работает это следующим образом.

Как ты сказал, у нас в репозитории, допустим,

То есть завершается транзакция, но на самом деле в нашем проекте это немножко посложнее, у нас в репозиториях транзакции не используются.

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

Начинаем в цикле пробегаться по нашим доменным событиям, где, как ты сказал, обезличенные iDomain визитеров.

Вызываем на этих интерфейсах метод асепт, передаем в него сам визитер.

Пытаемся внутри кастануть в специальную реализацию, если визитер ее не...

наследует, то как бы ничего не произойдет.

Ну, какой-то обработки не будет.

Если это нам необходимо, и это получилось внезапно, вот у нас есть Book Domain Event Visitor, он, собственно, успешно вызовется, примет это доменное событие и обработает, как его надо.

Собственно, делается это дальше следующим образом.

Как я говорил, накапливается список доменных событий, какой-то фоновый процесс, он

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

А он, соответственно, уже отвечает за работу с каким-то внешним хранилищем, с другой таблицей.

Репозитория отвечает за работу с таблицами, связанными с консистентностью, а эта материализация за...

Так сказать, то, что нам не важно, консистентно оно или не консистентно, но это в целом со временем исправляется.

Слушай, выглядит очень круто.

Сам придумал?

Ну, на самом деле, хотел бы я это сказать, но нет.

Я, конечно, знал про визитер, но подсмотрел...

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

Вот у нас, собственно, самый сложный пример.

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

Так что я думаю, мы сделали все наше дело, теперь действительно можно впроду.

Спикер 4

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

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

Больше денег заработаем.

Спикер 5

Было бы, конечно, прикольно, классно, стильно, и мы бы много заработали, но, наверное, нет.

Нам всё-таки придётся довольно много переписать, потому что это по смыслу что-то такое другое.

Там другой доменный язык, другие правила, другая вселенная, назовём это так.

Спикер 4

И...

Ну, кажется, я видел у Эванса тоже решение для этого.

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

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

А если у вас школьная библиотека, то это учебники, и у них есть, скажем, номер класса, для которого этот учебник и название предмета –

В общем, есть какие-то дополнительные поля.

Эванс предлагает выделить такое понятие, как смысловое ядро.

Вот вы берете эту сущность и каким-то способом разбиваете.

Вы выделяете общие атрибуты в этой сущности, такие, которые есть у всех вариаций этой сущности, а дополнительные атрибуты, каким-то другим образом надстраиваясь над ними.

Ну вот, мы выбрали такое понятие, как Extended Poly.

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

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

Но в слое приложения вы можете с этим работать, можете проверять, есть ли там какие-то специальные ключи значения.

Вот для каких-то специальных случаев можно что-то туда докинуть и дописать.

Ну и таким образом в нашей системе еще и модульность появляется.

Спикер 5

А что если ни в газетном киоске, ни в библиотеке внезапно так нет общих полей?

В книге, да, ты имеешь в виду?

Спикер 4

Ну, вообще говоря, если окажется так, что для разных проектов у одной сущности нет ничего общего, то значит, это разные сущности.

Вообще, вот вся эта история с DDD, я сказал, когда она нужна,

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

И при этом есть еще определенные требования к командам.

Обязательно нужна сильная аналитика.

У нас все команды комплектуются аналитиками.

И аналитическая проработка, вообще DDD очень чувствительна к аналитической проработке правильной.

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

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

Ну, собственно, вот мы еще и модульность сделали в проекте, смотрите.

Ну и получилось что-то такое.

Вот у нас есть BLL, он с помощью доменных событий отправляет данные в Query Source, там какое-то денормализованное представление.

Над этим всем есть Application, он занимается манипуляциями, манипулирует сущностями из BLL, читает из Query Source, ну и к Application обращается пользователь.

Он либо подает команды на изменение сущности, либо подает запросы на чтение сущностей.

Спикер 5

Слушай, Денис, я смотрю на слайд, тут действительно все как ты описал.

Мне кажется, в нашей микросервисной архитектуре тут, наверное, чего-то не хватает.

Покажи магию, пожалуйста.

Спикер 4

Давайте немножко отдалим.

Спикер 5

Но до сих пор чего-то не хватает.

Давайте еще раз.

Смотрите, мы в какой-то момент пришли к такому, по-некому, как фрактальная архитектура.

Мы, по крайней мере, так назвали.

Значит, у нас есть какой-то уровень приложения конкретного микросервиса, который исповедует так называемую «cleaner architecture».

Honor имеет три слоя, все прекрасно.

И все остальные сервисы точно так же.

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

Если мы еще выше на уровень систем, enterprise, архитектуры поднимаемся, то тем более получается.

И вот как бы такой пример.

У нас есть бизнес-слой, он превращается в сервисы доменной области.

Эти сервисы, они отвечают за правильное, скажем так...

правильное представление сущности нашего мира в своих терминах, назовем это таким образом.

Есть доменные события, которые прям ложатся на какие-то сообщения в наши брокеры сообщений.

Есть query-сорсы, которые становятся сервисами представлений.

Они даже, ну, можно и не сервис, можно один сервис.

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

Application слой превращается, мы назвали это сервис-сага.

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

Этот сервис-сага взаимодействует как с бизнес-слоем, так и с сервисом

И, соответственно, тот же CQRS, есть команды что-то меняющие, есть запросы, которые что-то читают.

Спикер 4

Ну да, таким образом, у каждого элемента взаимодействия внутри микросервиса в архитектуре есть какой-то эквивалент в виде элемента в устройстве одного микросервиса.

Можете по ссылке посмотреть пример сервиса.

Ну, там нереализованные репозитории, но вся основная вот эта вот оснастка, которую мы показывали, она есть.

Можно посмотреть, как работает визитер, как собирается материализованное представление, как там команда и запросы разделяются.

В общем, если интересно, заглядывайте.

Спасибо за внимание.

Спикер 3

Ребята, спасибо большое за такой насыщенный доклад.

Мне кажется, огромное количество вопросов сейчас на вас посыпется.

Я думаю, один мы, наверное, успеем задать здесь.

И вот я вижу руку молодого человека в втором ряду.

Поднял ее самым первым еще заранее.

Как чувствовал?

Спикер 4

Давайте.

Спикер 2

Георгий из Озонтеха.

Во-первых, спасибо за доклад.

Очень интересно.

Вначале вы говорили, что у вас в спецификациях используются экспрессионы.

И я так понимаю, что, скорее всего, как ОРМКУ и ЕФ используете для этого.

Либо как-то констомные транспиляторы используете.

Вопрос заключается в том, что

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

Соответственно, как избавляетесь от неявных зависимостей на каких-то инфраструктурных штуках, конкретно вот, например, на базу данных, потому что сказали, что репозитория не реализована, это на самом деле самая интересная часть была.

Спикер 4

Ну да, смотрите, у нас в реальном проекте спецификации пока что только для фильтров используются, они у нас лежат в application слое, но манипулируют только доменными моделями, больше никакими.

Соответственно, в application слое составляется комбинация выражений, и она передается в инфраструктуру.

То есть инфраструктура принимает просто такой expression.

Он никак не связан с entity framework, никак не связан с какой-то реализацией базы данных, с которой мы работаем.

У нас когда-то была Mongo, потом

Ее не разрешили использовать, мы перешли на JSONB-поля.

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

Раньше это была команда к драйверу MongoDB, то есть она на вход принимает выражение expression и на выход выдает кусок этого выражения для драйвера MongoDB.

А сейчас это выдает кусок SQL-запроса для JSONB-полей.

К сожалению, Entity Framework не поддерживает пока что такую гибкую работу с JSONB, чтобы прямо на методах сделать.

И поэтому мы немножко откатились к составлению запроса в виде такой длинной строки с кусочков.

Спикер 2

То есть реализуете отвязку через какой-то кастомный транспилятор.

Что еще раз?

Реализуете отвязку зависимости от инфраструктуры через кастомный транспилятор.

Спикер 4

Да, по сути да.

Спикер 3

Сильно.

Не знаю, может быть, еще вопросы успеваем?

Да, наверное, мы успеем.

Еще один вопросик.

Спикер 1

Здравствуйте.

Алена Избивайна.

Можете что-нибудь сказать по поводу производительности этого всего?

Спикер 4

Хороший вопрос.

У нас в первой версии доклада про это было.

Вообще конкретно наши кейсы не нагруженные.

То есть у нас сколько максимум человек?

Ну там единицы, десятки тысяч редко.

Больше не бывает пользователей.

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

То есть варианта применения в хайлоуде мы сами не пробовали.

Ну и навскидку выглядит как будто там нужны чуть-чуть другие

подходы.

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

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

не в реалтайме перестраивается, а синхронно.

Соответственно, тоже нужно дождаться, когда оно перестроится.

И вот это тоже нюанс.

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

Спикер 1

Спасибо.

Спикер 3

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

А тех, кто у нас присутствует онлайн, еще и оценить этот доклад в вашем интерфейсе.

Всем спасибо.