В статье рассматривается разработка доменных моделей в соответствии с тактическими паттернами 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.