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

От теоретической к прикладной доменологии

Для прикладного разработчика продукт состоит из сервисов, классов, таблиц. Для того, кто проектирует корпоративную архитектуру продукта, важны в первую очередь сущности и взаимосвязи между ними. Сущности объединяются в более крупные архитектурные единицы — домены. Каждый домен олицетворяет некую предметную область — интуитивно понятно, что в банковском приложении сущность «вклад» и сущность «кредит» близки между собой, а сущность «клиент» лежит немного в другой плоскости.

Доменная модель — в высшей степени практическая штука. Абстрактные «сущности» могут быть воплощены в железе и коде, и полезны для того, чтобы все участники процессов понимали, с чем работают. Любая модель — это пример коммуникации, которая позволяет участникам накладывать и снимать ограничения на процессы и их реализацию. Такие высокоуровневые модели, как доменная модель, редко реализуются в коде «as is», но они помогают в более детальном проектировании процессов и систем.

Работая с Data Fusion, мы многое узнали о том, какие сущности и взаимосвязи чаще всего встречаются и наиболее важны в проектах такого типа. Это знание воплощено в доменной модели Песочницы данных АБД.

Домены Песочницы данных

Сущности Песочницы подразделяются на следующие домены:

Обожаю домены! Вот они слева направо: Entity, Audit, Infrustructure…
Обожаю домены! Вот они слева направо: Entity, Audit, Infrustructure…
  1. Legal, commercial and regulatory domain — разнообразные юридические вещи. Законы, договора, права и правила.

  2. Audit Domain — аудит ресурсов, активов, доступов.

  3. Entity Domain — агенты и их роли; кто и в каком качестве работает в Песочнице

  4. Infrustructure domain — физические мощности, сервисы, инструменты. Всё, что необходимо для работы.

  5. Processing Domain — то, что касается непосредственно процесса Data Fusion, превращения данных в артефакты.

  6. Project domain — проекты Data Fusion, политики и требования, согласно которым они реализуются

  7. Providing Domain — предоставление прав доступа и контроль за ними.

Дальше мы поговорим о каждом домене подробнее.

Legal, commercial and regulatory domain

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

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

Схема отношений между сущностями в домене Legal, commercial and regulatory

Asset

Центральная сущность домена. Asset — это актив, интеллектуальная собственность. Например — датасет, модель данных или её коэффициенты.

Актив имеет ценность, иначе говоря — состоит в отношении hasValue с сущностью AssetValue. На актив кто‑то имеет права — соответственно, он состоит с сущностью Right в двусторонних отношениях applyTo (право применяется к активу) и isCoveredBy (актив покрывается правом). В дальнейшем я для краткости буду чаще всего опускать названия отношений, описывая только их суть.

Разумеется, никто не станет класть в Песочницу ценный актив просто так — ожидается, что с ним будут произведены некие манипуляции, генерирующие добавочную стоимость. Эти манипуляции описываются сущностью ProcessingObject — она принадлежит другому домену, Processing Domain. Манипуляции происходят не сами по себе, они производятся неким действующим лицом (Agent). Поэтому Asset и Agent состоят в отношении hasContributor.

Как гласит народная мудрость, «доверяй, но проверяй». Актив может (и будет) подвергаться проверке, и с ним будут связаны отчёты о проверке (AuditReport). Это тоже сущность из другого домена — Audit Domain.

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

Помимо отношений, Asset имеет собственные атрибуты. Кроме стандартных id, title, description есть ещё атрибут assetType — тип актива, выбираемый из конечного списка типов (датасет, модель и т. п.).

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

AssetValue

Казалось бы, ценность актива можно было сделать простым числовым атрибутом. Но — только казалось бы. Помимо вопроса «сколько?» (ответ на который кроется в атрибуте assetValueAmount), ценность должна отвечать ещё и на другие вопросы.

«Сколько чего?» — атрибут assetValueCurrency. Очевидно.

«Сколько за что?» — атрибуты assetValueName и assetValueDescription. Например, если Asset — это датасет абонентов Мегафона, то assetValueName — «Персональные данные абонентов Мегафона», а assetValueDescription поясняет, почему ценны персональные данные. При конфигурировании проекта можно легко изменять значения. Это позволяет бизнесу доносить до IT особенности процессов и причины тех или иных запросов.

«Сколько когда?» — атрибут assetValueVersion. Ценность актива меняется во времени, и это должно иметь отражение в доменной модели.

«Сколько…» Ладно, тут не получается придумать вопрос. Так или иначе, ценность имеет тип, который выбирается из закрытого перечня типов.

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

Right

Права на пользование активом. Права не появляются сами по себе, они порождаются контрактом (Contract). Права выдаются агентам (Agent) и зависят от нормативно-правовых актов (Regulation).

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

Regulation

Собственно, нормативно-правовые акты. Они могут быть как внешними (постановления правительства), так и внутренними (протоколы комитетов, распоряжения Правления и т.п.). На НПА основываются права (Right) и правила (Rule). Атрибуты довольно ожидаемые — имя, номер, описание, ссылка, тип, выбираемый из закрытого перечня.

Rule

Если права регулируют использования конкретного актива, то правила определяют действия, бездействия или ограничения, которые могут быть указаны в контракте. Помимо очевидных атрибутов (имя, описание, ссылка, тип), есть ещё необязательный атрибут ruleStatement (строгая спецификация) и rulePriority. Приоритет определяет, какому из нескольких правил следовать в случае конфликта. Это позволяет создавать сложную, но при этом работающую систему правил, в частности — наследовать правила.

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

Contract

Наконец мы добрались до сущности, которая много раз упоминалась выше. Контракт — это то, благодаря чему Data Fusion возможен в юридическом смысле. Контракт определяет правила (Rule), порождает права (Right) и требования (Requirement, это другой домен, о них позже). Контракт имеет стоимость (ContractCost), заключается агентами (Agent), может ссылаться на другой контракт в качестве шаблона.

Интересных и неожиданных атрибутов у контракта нет.

ContractCost

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

Необходимо понимать, что сущности из Legal, commercial and regulatory domain — это своего рода метаданные. Они не используются в «айтишной» части песочницы напрямую. Скажем, если существует Regulation, что биометрические данные должны обрабатываться на оборудовании с аппаратным шифрованием, технически это не помешает Песочнице обработать эти данные на обычном оборудовании. Однако это нарушение будет выявлено в процессе аудита.

К слову об аудите…

Audit Domain

Это небольшой, но очень важный домен. Песочница данных — не только железо и софт. В первую очередь Песочница — это доверие. Чтобы Data Fusion стал практически возможен, владельцы данных должны верить, что с их интеллектуальной собственностью не произойдёт ничего плохого. А доверие, как я уже говорил выше, основано на проверке. Лучше даже не на одной, а на многочисленных проверках, обязательных и регулярных.

 

Схема домена Audit
Схема домена Audit

AuditJob

Аудиту могут подвергаться самые разные вещи — активы, ресурсы, доступы. Различными могут быть и цели аудита. Может оцениваться стоимость актива, может проверяться соответствие контракта нормативно‑правовым актам, или то, выполняются ли в проекте требования.

Аудит — это разновидность обработки данных.Поэтому сущность AuditJob наследуется от сущности ProcessingJob из Processing domain. Аудит может порождаться требованием (Requirement, сущность из Project domain).

Один из возможных вариантов расширения модели предполагает использование аудита для комплаенса. Например — юридического или финансового Due Diligence. Но в таком случае придется отвязать его от ProcessingJob. Если вам понадобится расширить аудит на сущности в Legal, commercial and regulatory domain типа Contract, возможно, лучшим решением будет выделить его в отдельную сущность этого домена.

Результатом аудита всегда становится AuditReport. Результат может иметь количественное выражение (Measure). Аудит может быть связан с другими аудитами. Например, уточнять результаты предыдущего.

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

AuditReport

Отчёт о проведённом аудите. Может быть связан с другими отчётами. Может ссылаться на политику (Policy, сущность из Project Domain), согласно которой был составлен. Может касаться актива (Asset) или объекта обработки (ProcessingObject).

Помимо имени, типа и описания, имеет дату и ссылку на файл с полной версией отчёта.

Measure

Числовая оценка — например, успешность прохождения проверки по десятибалльной шкале. То, как именно вычисляется эта оценка, может определяться политиками (Policy). Оценка может быть нужна согласно требованию (Requirement).

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

Мы разобрали уже два «вспомогательных» домена, которые очень важны для функционирования Песочницы, однако никак непосредственно не касаются Big Data. Самое время поговорить о третьем таком домене.

Entity Domain

Неприятное свойство работы — сама себя она не сделает. Entity Domain описывает тех, кто в нашей системе что‑то делает, иначе говоря — агентов. И поясняет — что им делать можно, а что нельзя.

 

Схема домена Entity
Схема домена Entity

Agent

Центральная сущность домена. Агент — сущность «социальная»: у него мало собственных атрибутов (классическая четвёрка id‑name‑type‑definition), зато много разнообразных взаимосвязей с другими сущностями из различных доменов.

Агент имеет роль (Role) и следует политике (Policy). Как уже говорилось выше, агент может выступать участником контракта (Contract). Агент может обслуживать объект обработки (ProcessingObject). Агент может инициировать события CRUDE (CRUDEEvent из Providing domain). Обратите внимание на расширенный акроним: E значит Exchange. Агент может выдавать права доступа (Credential, опять же из Providing Domain). Может владеть сервисами (Service из Infrustructure Domain) и разрабатывать объекты планов (PlanObject).

Иногда агент может быть частью другого агента. Но об этом ниже.

Person

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

По сравнению с Agent в Person добавляются атрибуты, хранящие персональные данные — имя, фамилию, телефон и электронный адрес. Одна из причин, по которым имеет смысл выделять сущность Person — обращение с персональными данными регламентируется законом.

Organization

Конечно, любая организация состоит из людей, но иногда с точки зрения ролевой модели удобно воспринимать её как единое целое. По сравнению с Agent в Organization добавляется атрибут organizationName.

Crew

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

Теоретически в Crew могут входить и Organization — например, если несколько организаций объединились для работы над общим проектом.

Эта сущность выглядит несколько искусственной, однако она важна в контексте Data Fusion. Команды, которые выступают агентами, могут иметь разные права и политики. Например, команды организаций, объединяющих датасеты, не должны иметь доступ к «чужому» датасету. А команда дата саентистов имеет доступ к обоим датасетам. Crew помогают более гибко конфигурировать агентское взаимодействие, реализуя сложные зависимости.

Role

Помимо разновидностей агентов, в Entity domain входят и роли. Роли — удобный способ управления правами доступа. Вместо того, чтобы каждому отдельному агенту рассказывать, что ему можно, а что нельзя, ему назначается роль, в которой всё уже прописано.

Роли определяются политиками (Policy). Однако политики только создают роли, но не наделяют ими агентов. Агентам присваиваются роли исходя из требований (Requirements), которые, в свою очередь, определяются в контрактах.

Роли наделяются правами доступа (Credentials), которые автоматически переходят к агентам, получившим эти роли. Кроме того, требования (Requirements) могут давать ролям дополнительные полномочия.

Атрибуты сущность Role — имя, тип, определение.

Теперь, когда мы закончили со «вспомогательными» доменами, можно переходить к основным.

Infrustructure domain

В прошлом параграфе мы говорили о том, что работа сама себя не сделает. Ещё одна плохая новость: большинство работы не делается голыми руками, для неё требуются ресурсы и инструменты. Infrustructure domain описывает техническую среду, доступную агентам.

Взаимоотношения сущностей домена Infrustructure
Взаимоотношения сущностей домена Infrustructure

Resource

Ресурс — это то, что потребляется при функционировании Песочницы. Объём CPU или дискового пространства, ресурсы процессора и т. п. Ресурс — частный случай актива, потому эти сущности связаны отношением наследования. Ресурсы выделяются в соответствии с требованиями (Requirement) и могут ограничиваться контрактами (Contract). Ресурсы выступают ограничениями для политик (Policy). Материя властвует над мыслью: никакая политика не может даровать больше дискового пространства, чем у нас физически есть.

Различные сущности потребляют ресурсы. Объекты обработки (ProcessingObject) могут потреблять ресурсы просто самим фактом своего существования. Обычно это касается дискового пространства. Процессы обработки (ProcessingJob) потребляют ресурсы во время своего исполнения. Ещё ресурсы потребляются сервисами (Service) и инструментами (Tool).

Помимо имени, описания и типа, ресурс имеет два более интересных атрибута: resourceLimit и resourceCapacity. Оба атрибута ограничивают использование ресурса. Однако первый из них проистекает из требований и политик, а второй — из ограничений реального мира.

Service

Что‑то, что помогает в обработке ProcessingObject, реализует какие‑то функции, бизнес‑логику. Примером сервиса может служить облачное S3-хранилище. Сервисы потребляют ресурсы (Resource) и принадлежат агентам (Agent). Атрибуты сервиса — имя, тип, описание и (что особенно полезно) ссылка на документацию по его использованию.

Tool

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

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

Processing domain

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

 

Схема взаимоотношений в домене Processing
Схема взаимоотношений в домене Processing

ProcessingObject

Разновидность Asset. Сущность высокой степени абстракции — нечто, подвергаемое обработке. Выборка, датасет, информация.

Как уже говорилось выше, ProcessingObject потребляет ресурсы (Resource) и обслуживается агентом (Agent). Объект обработки может подвергаться аудиту (который, в принципе, является частным случаем обработки) — тогда после аудита с ним будет связан AuditReport. К объекту могут быть предоставлены права доступа (Credentials из Providing domain). Эти права доступа расширяют (amplifies) объекты обработки, добавляя к ним метаданные.

ProcessingObject может быть ассоциирован с активом (Asset). С объектами обработки ассоциируются события CRUDE (CRUDEEvent). Объект обработки может быть входным или выходным для процесса обработки (ProcessingJob, см. далее).

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

Атрибуты объекта обработки — тип и версия. Ещё у ProcessingObject есть описание, но оно вынесено в отдельную сущность Description.

Description

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

Требования (Requirement) могут уточнять описание — что именно в нём должно быть указано, с какой частотой оно должно пересоздаваться. Плюс, описание может ссылаться на другое описание как на шаблон.

Атрибуты Description — ссылка на документ и descriptionTrigger. То есть ссылка или наименование события, которое запускает описание объекта обработки.

ProcessingJob

Самое сердце Data Fusion. На входе этого процесса может быть ProcessingObject. Впрочем, может и не быть: существуют процессы, обрабатывающие ресурсы. Например, журналирование. На выходе может быть новый ProcessingObject, или, если повезёт — артефакт (Artefact).

Процесс обработки вначале планируется. Это олицетворяется объектом планирования (PlanObject из Project Domain). Он потребляет ресурсы, может требовать участия агентов. CRUDEEvent может служить сигналом к запуску ProcessingJob.

Атрибуты процесса обработки — имя, описание, тип.

Artefact

Не Святой Грааль, но только чуть‑чуть не дотягивает до этого звания. Артефакт — это разновидность Asset, которая может получиться в результате исполнения ProcessingJob. Пример артефакта — обученная модель.

Несмотря на свою ценность, с точки зрения доменной модели артефакт — самая простая сущность. У него единственное отношение. Он — результат процесса обработки. И, не считая id, всего один атрибут — описание цели его производства.

Project domain

С точки зрения информатики производство артефакта стартует в момент, когда первый ProcessingObject поступает на вход первой ProcessingJob. Однако в жизни всё начинается гораздо раньше — с планирования, формулировки гипотез, создания проектной документации. Именно эта сфера деятельности описывается сущностями из Project Domain.

 

Схема взаимоотношений в домене Project
Схема взаимоотношений в домене Project

PlanObject

Объект планирования — описание того, что предстоит сделать. Объекты планирования создаются агентами (Agent) и порождаются требованиями (Requirement). На них влияют политики (Policy), но и сами объекты планирования могут порождать новые политики.

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

Policy

Это слово звучало уже много раз, пришло время с ним разобраться. Политики — это представления о том, как нужно реализовать определённый проект. Эти представления формируются агентами (Agent) исходя из совокупности многих факторов.

Во‑первых, требования (Requirement), проистекающие из контракта. Во‑вторых, правила (Rule), источник которых — нормативно‑правовые акты. В‑третьих, ограничения, накладываемые ресурсами (Resource). Наконец, в‑четвёртых — планы агентов, сформулированные в PlanObject.

Политики, в свою очередь, сами могут порождать новые требования и влиять на объекты планирования. Например, политика управления доступом порождает требования к компетенции агентов и распределению ролей. Политики определяют роли (Role) и права доступа (Credentials) — но только задают возможные варианты, а не распределяют их по конкретным агентам. На политики ссылаются отчёты по аудиту (AuditReport) и их оценки (Measure).

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

Requirement

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

Про отношения сущности Requirements к другим сущностям, в принципе, всё уже было рассказано выше. Требования определяют необходимые ресурсы (Resource), в соответствии с ними запрашиваются права доступа (Credentials), они могут наделять неспецифичными полномочиями роли (Role). В требованиях может быть указана необходимость аудита (Audit), спецификация описания ProcessingObject. В соответствии с требованиями может устанавливаться оценка (Measure). Также требование может ссылаться на другое требование в качестве шаблона.

Атрибуты у требования такие же, как у политики.

Providing Domain

Этот домен можно представить себе как некий железнодорожный узел — место, где сходятся Entity, Project и Processing domain. В соответствии с проектом агентам предоставляются права на обработку данных.

 

Схема взаимоотношений в домене Providing
Схема взаимоотношений в домене Providing

CRUDEEvent

Сущность, олицетворяющая события CRUDE. События генерируются в процессе обращения агентов к активам. Соответственно, каждое такое событие связано с агентом, который его инициирует, и с объектом обработки, к которому он обращался. У событий двойная связь с правами доступа (Credentials, см. ниже).

События могут запускать процесс обработки (ProcesingJob). Они могут быть ассоциированы с другими событиями (например, чтение файла.ipynb и выполнение кода в JupyterLab).

Атрибуты события — имя, описание, тип. И то, было ли оно успешным, то есть не завершилось ли ошибкой.

DownloadEvent и UploadEvent

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

Credential

Права доступа к ресурсам Песочницы. Набор возможных прав доступа определяется политиками (Policy). Сами права доступа выдаются агентами (Agent) на основании требований (Requirement). Они относятся к объектам обработки (ProcessingObject) и присваиваются ролям (Role).

Права доступа могут ссылаться на другие права как на шаблон и объединяться в группы.

Отношение прав доступа к событиям CRUDE двоякое. С одной стороны, права доступа определяют, какие события в принципе возможны. С другой — права доступа авторизуют эти события.

Пример использования модели в проектировании

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

 

Обычный день в Песочнице данных
Обычный день в Песочнице данных

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

Начнём с того, что датасет подлежит обработке, а потому является экземпляром сущности ProcessingObject. Далее, датасет нужно где‑то хранить — а значит, он своим существованием потребляет ресурс storage, который, само собой, является экземпляром сущности Resource.

Для того, чтобы обработать AbonentDataset, инициируется процесс fillNaN (экземпляр ProcessingJob). Этот процесс, пока он в работе, потребляет ресурсы процессора и оперативной памяти (vCPU1 и vRAM1, также являющиеся Resource). Кроме того, в этом процессе используется инструмент JupyterNotebook (сущность Tool). Этим инструментом опять же потребляются ресурсы (vCPU2 и vRAM2)

Когда процесс fillNaN завершился, у нас появляется новый датасет, обозначенный на диаграмме как NewAbonentDataset. Соответственно, для fillNaN входным объектом является AbonentDataset, а выходным — NewAbonentDataset. Новый датасет также нуждается в хранении, и потому потребляет ресурс storage. Наконец, старый и новый датасеты связаны отношением isVersionOf.

Заключение

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

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

Комментарии (0)