В статье рассматривается разработка доменных моделей в соответствии с тактическими паттернами DDD — Value Object
и Entity
, с целью создания собственной строгой иерархии функциональной системы типов (ФСТ). Предложен фреймворк, включающий методологию проектирования и кодогенерации доменных моделей на языке Котлин. Рассматриваются вопросы и ставятся задачи … организации, планирования, документирования, моделирования, регулирования, управления, оценка результата [Д] как код (DSL). Разработка фреймворка ведется в значительной части на основе научных работ Д.А. Новикова по организации, управлению и методологии деятельности, приведенных в конце статьи. Используемые понятия, термины и определения основаны на [5] и выделяются полужирным шрифтом. Общие термины и понятия, которые легко ищутся в поисковых системах, выделяются курсивом. Слова, относящиеся к кодовой базе, выделяются обратными кавычками
.
Введение
В сложной иерархически организованной системе рост разнообразия на верхнем уровне обеспечивается ограничением разнообразия на предыдущих уровнях, и наоборот, рост разнообразия на нижнем уровне разрушает верхний уровень организации (то есть, система как таковая гибнет).
— Закон Е. А. Седова
В парадигме DDD доменному слою отводится наиважнейшая роль и по этому проектирование и реализация доменных моделей нуждается в особой тщательности и строгости. Применение прогрессивных парадигм и концепций в разработке ПО, таких как SOLID, Clean Architecture, DDD, DSL, Функциональное программирование, заветов Мартина Фаулера, Роберта Мартина и др., дает неоспоримое преимущество и выводит разработку ПО на новый технологический уровень, позволяя существенно повысить адаптивность ПО, скорость его разработки, снизить количество багов. Важно обеспечить модульность, соблюдение принципа Low Coupling & High Cohesion, недопускать Primitive Obsession.
В условиях непредсказуемости и изменчивости внешней среды, потребностей пользователей, конкурентной борьбы, выживаемость ИТ-проектов определяется способностью к быстрой адаптации. Именно за это отвечает Core Domain. Для этого код и процесс его создания должны обладать следующими характеристиками и свойствами:
Чистый, хорошо структурированный, читаемый код. Есть эмпирическое положение, что 90% времени разработчики тратят на чтение кода. Зачастую — своего собственного. Первоисточник этого утверждения найти не удалось, но оно согласуется с законом Парето и личным опытом. Причин этому множество: это и тезис создателя Perl Ларри Уолла: "We will encourage you to develop the three great virtues of a programmer: laziness, impatience, and hubris.", внешнее давление — дедлайны, TTM, иллюзия менеджмента, что Proof of Concept можно выпускать в прод, низкий технологический уровень производства ПО, и т.д.
Согласованность документации и кода с кросс-ссылками. Наличие базы знаний проекта.
Покрытие юнит-тестами.
Разделение труда, распределенное по ролям. Упорядочивание деятельности с выделением фазы проектирования, стадий и этапов (С.м. [4], стр. 255).
Разделение на абстракцию и имплементацию со своими жизненными циклами разработки и релиза. Хорошо организованное API.
В целях краткости изложения вводятся понятия Агент — субъект, пользователь представленного фреймворка. В качестве Агента может выступать как индивид, команда разработки, ИТ-отдел, ИТ-компания или информационные системы такие, как ИИ-агенты. УМ (Улиточная Модель) — разрабатываемая Агентом доменная модель его предметной области на основе представленного фреймворка. Название «улиточная» обусловлено фрактальным, вихревым, рекурсивным характером структуры моделей.
Статья, хоть и имеет весьма узкую, специфическую тему, предназначена не только для разработчиков на Котлин, практикующих DDD, но и для аналитиков, руководителей и владельцев проектов.
User Story
Мотивация
Постановка цели
Определение задач
Реализация абстракции:
ФСТ, k3dm
Функционал
Регламент концепций
Реализация имплементации
Билдеры: основной, DSL, JSON
Примеры
Выводы
Текущее состояние
О проекте
Обозначенные выше положения вызвали появление идеи создания фреймворка для разработки доменных моделей, выработке методологии и технологии, что позволяет значительно сократить расходы на производство ПО за счет организации и автоматизации деятельности и, тем самым, сведения ее к регулярному виду. Идея породила формирование потребностей, которые можно выразить в виде следующих пользовательских историй (User Story).
Пользовательские истории
Как разработчик я хочу |
чтобы |
---|---|
Иметь ограничения в виде требований, норм, принципов |
в результате получать чистый код и, тем самым, снизить цену владения. |
Иметь базовое основание ФСТ |
проектировать собственную УМ. |
Создавать концептуальные доменные модели в виде собственной строгой иерархии ФСТ и компилируемого кода |
писать функционал (бизнес-логику) в парадигме ФП и тестировать их юнит-тестами на самой ранней стадии. |
Иметь разделение доменных моделей на абстракцию и имплементацию |
обеспечить гибкость (High Cohesion & Low Coupling), разделение труда и ЖЦ разработки. |
Иметь механизм валидации при создании объектов доменных моделей |
реализовывать бизнес-логику. |
Автоматически генерировать код имплементации концептульных моделей, включающий DSL и сериализацию/десериализацию |
сократить затраты на ручной труд, обеспечить гибкость и адаптивность моделей к изменениям внешней среды. Применять парадигмы функционального программирования. |
Мотивация
Котлин, являясь языком со строгой типизацией с одной строны, является языком общего назначения для самого широкого применения. С другой стороны, такая широта открывает возможности для написания смердящего (code smell), опасного и запутанного (spaghetti code) кода, превращая кодовую базу в Big Ball of Mud. В таком коде имеет место:
нарушение принципов SOLID,
нарушение инкапсуляции, мутабельность свойств и объектов,
Primitive Obsession — прямое использование встроенных примитивных типов (
String
,Int
,Boolean
, и т.д.) и типов общего назначения (File
,Url
,UUID
, и т.п.),отсутствие разделения на абстракцию и имплементацию.
Эти порочные практики во многом обусловлены желанием «срезать угол» из-за значительных трудозатрат (рутинного ручного труда) по их недопущению. Предлагаемый фреймворк позволяет избежать этого.
Цель
Цель проекта — создание фреймворка, который включает методологию разработки УМ и инструмент кодогенерации.
Задачи
Создать библиотеку базовой ФСТ для тактических DDD-паттернов Entity и Value Object.
Разработать методологию и регламент проектирования УМ.
-
Разрабртать кодогенератор финального кода, включающего следующий функционал:
опции кодогенератора, позволяющие настраивать различные правила формирования выходного результата: имена классов, имена пакетов, и пр.,
-
кодогенерация:
имплементирующих классов,
классических билдеров (паттерн Строитель),
DSL-билдеров,
сериализации/десериализации
Создать работающие примеры использования.
Технологии
Реализация
Допустим, требуется создать некоторую модель (тип данных) с определенными свойствами (Value Object), которую можно реализовать таким образом:
data class MyType(
//
val name: String,
val count: Int,
val components: Map<UUID, NestedType>
) {
data class NestedType(
val myFile: File,
val desc: String
)
}
val myType = MyType(
"my name",
1,
mapOf(
UUID.randomUUID() to MyType.NestedType(File("/path/to/file1"), "file 1"),
UUID.randomUUID() to MyType.NestedType(File("/path/to/file2"), "file 2"),
)
)
Такая модель обладает всеми недостатками, изложенными выше, а именно:
отсутствие разделения на абстракцию и имплементацию,
открытая мутабельность свойств,
Primitive Obsession,
отсутствие механизма валидации при создании объекта,
создание объектов выполняется напрямую через открытый конструктор,
отсутствие DSL
отсутствие механизма сериализации/десериализации
Применять такую модель можно, разве что, в рамках Proof of Concept (PoC). С применением предлагаемой методологии такая модель будет выражена таким обоазом:
// Проектируемая разработчиком абстракция
// Проектируемый разработчиком функционал
Кроме того, хотелось бы иметь
Библиотека корневых типов
Библиотека базовой ФСТ для тактических DDD-паттернов Entity и Value Object представлена в проекте k3dm. Она состоит из трех корневых интерфейсов для наследования при проектировании УМ. Эти интерфейсы являются маркерными для работы кодогенератора и обеспечивают закрытость проектируемой системы типов УМ. Библиотека также содержит набор аннотаций для управления поведением кодогенератора. Переопределение метода validate()
позволяет задать правила валидации при создании объектов проектируемой модели. Метод вызывается в конструкторе класса имплементации и должен выкидывать исключение при нарушении заданных правил.
Методы fork()
и apply()
являются техническими, их имплементация создается кодогенератором. Эти методы необходимы при создании функционала моделей на уровне абстракции, когда имплементация еще не создана (сгенерирована) и обеспечивают мутабельность через копирование объекта с заданием новых свойств. Также имеется аннотация @Neutral
, благодаря которой будет сгенерирован объект типа по умолчанию. Такой синглтон предназначен для задания нейтрального элемента типа. Этот подход известен как witness pattern.
Как используется эта библиотека будет показано ниже в изложении и в примерах.
Функционал
Фреймворк позволяет
Лучше всего показать принцип и работу фреймворка на наглядных примерах. Допустим, необходимо создать модель геометрической точки с двумя координатами x и y, выраженных в целых числах. Функционалом модели будут арифметические операции и операция расчета расстояния между двумя точками. Для этого определяются интерфейсы моделей, которые наследуются от соответствующих типов базовой биб
interface Point : ValueObject.Data {
val x: Coordinate
val y: Coordinate
@Neutral
val neutralDistance: Distance
override fun validate() {}
operator fun plus(other: Point): Point =
fork(x + other.x, y + other.y)
operator fun minus(other: Point): Point =
fork(x - other.x, y - other.y)
operator fun times(other: Point): Point =
fork(x * other.x, y * other.y)
interface Coordinate : ValueObject.Value<Int> {
override fun validate() {}
operator fun plus(other: Coordinate): Coordinate =
apply(boxed + other.boxed)
operator fun minus(other: Coordinate): Coordinate =
apply(boxed - other.boxed)
operator fun times(other: Coordinate): Coordinate =
apply(boxed * other.boxed)
}
interface Distance : ValueObject.Value<Double> {
override fun validate() {
val range = 0.0..300.0
check(boxed in range) { "Distance not in range $range" }
}
operator fun plus(other: Double): Distance =
apply(boxed + other)
}
}
Анализ проблем
Боль
добавление нового функционала с каждым разом все труднее (костыли)
не тот результат (иллюзии)
нехватка ресурсов — время, кадры (цена владения)
демотивация команды разработки (костыли, иллюзии)
Растерянность. Потеря во времени и пространстве (иллюзии).
Страх
Порождают неадекватные решения
Боль. Наступить на грабли.
Неизвестность. Негативный опыт.
Быть обманутым. Риски. Принять неправильное решение. Рождает карго-культ.
Недостичь результата / получить не тот.
Сроки
Причины
Кустарщина (неадекватность технологии - некомпетентность)
высокая цена владения кодовой базы (костыли)
PoC в проде и необходимость его поддерживать (неправильные решения)
Иллюзии. Очковтирательство. Потеря управления — нет учета и контроля реальности
Карго-культ (неправильные решения)
Основная причина краха проектов не в команде, а в технологиях. Если ваш проект катится не туда - пересмотрите подходы. Все можно вылечить. Сайт проекта: https://github.com/tepex/kddd-ksp-dev/tree/new-arch
Со своей стороны готов оказать помощь в рефакторинге кодовой базы и архитектуре: https://t.me/tepex
Понятия, термины и определения
Агент
Субъект разработки УМ. Роль (аналитик, архитектор, разработчик), ОТС (в терминологии [1] стр. 12).
УМ
Улиточная модель (Helix Model). Доменная модель предметной области Агента в рамках ФСТ и в парадигме тактических DDD-паттернов Value Object и Entity.
ФСТ
Формальная система типов (Formal Type System).
Список источников и литературы
Белов М.В., Новиков Д.А. Методология комплексной деятельности. М.: ЛЕНАНД, 2018.
Николенко В.Ю. Секреты успешных НИОКР. [б.м.]: Издательские решения, 2023.
Новиков А.М., Новиков Д.А. Методология: словарь системы основных понятий. М.: Либроком, 2013.
Новиков Д.А. Теория управления организационными системами. М.: МПСИ, 2005.
Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. СПб.: Питер, 2023.
Теория управления. Дополнительные главы. Авторский коллектив под ред. Новикова Д.А. М.: ЛЕНАНД, 2019.
Khononov V. Learning Domain-Driven Design. O’Reilly Media Inc, 2021.
Комментарии (5)
AndreyPMI
10.08.2025 13:56Простите, но это не несёт никакой ценности, ни научной, ни практической. Уже на этапе просмотра примера с дата классом (тут и вар поля и вложенный дата класс в другой дата класс), можно сделать выводы, что статья написана нейросетью. Если разработчик ты позволял себе такие конструкции, то его не спасло бы ни синтаксис языка, ни фреймворки, ни научная публикация.
kloun_za_2rub
Решается какая-то выдуманная проблема. Зачем то для data классов нужна абстракция, логика в них же почему то должна быть(валидация). Почему то в data class используется var, когда все всегда используют только immutable поля(val). Проблемы сериализации решаются специальными библиотеками, заточенными под это.
При этом почему то библиотека называется фреймворком. Для меня фреймворк это то, что имеет свою базу, а вам позволяет эту базу заполнять/расширять. В вашем же случае это не так.
При этом всем вся статья выполнена в академическом стиле, что читать довольно сложно если честно
Tepex Автор
var -- это ошибка на этапе написания.
kloun_za_2rub
Ну нет же, у вас в проблемах такой реализации написано "открытая мутабельность свойств", но ее нет. Можно только создать новый объект-копию с измененными параметрами если использовать val
Tepex Автор
Да, так и задумано. Копия.