Как стать автором
Поиск
Написать публикацию
Обновить

Проектирование и кодогенерация DDD-моделей на Kotlin

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров1.8K

В статье рассматривается разработка доменных моделей в соответствии с тактическими паттернами DDD — Value Object и Entity, с целью создания собственной строгой иерархии функциональной системы типов (ФСТ). Предложен фреймворк, включающий методологию проектирования и кодогенерации доменных моделей на языке Котлин. Рассматриваются вопросы и ставятся задачи …​ организации, планирования, документирования, моделирования, регулирования, управления, оценка результата [Д] как код (DSL). Разработка фреймворка ведется в значительной части на основе научных работ Д.А. Новикова по организации, управлению и методологии деятельности, приведенных в конце статьи. Используемые понятия, термины и определения основаны на [5] и выделяются полужирным шрифтом. Общие термины и понятия, которые легко ищутся в поисковых системах, выделяются курсивом. Слова, относящиеся к кодовой базе, выделяются обратными кавычками.

Введение

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

— Закон Е. А. Седова

В парадигме DDD доменному слою отводится наиважнейшая роль и по этому проектирование и реализация доменных моделей нуждается в особой тщательности и строгости. Применение прогрессивных парадигм и концепций в разработке ПО, таких как SOLIDClean ArchitectureDDDDSLФункциональное программирование, заветов Мартина ФаулераРоберта Мартина и др., дает неоспоримое преимущество и выводит разработку ПО на новый технологический уровень, позволяя существенно повысить адаптивность ПО, скорость его разработки, снизить количество багов. Важно обеспечить модульность, соблюдение принципа 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 — прямое использование встроенных примитивных типов (StringIntBoolean, и т.д.) и типов общего назначения (FileUrlUUID, и т.п.),

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

Эти порочные практики во многом обусловлены желанием «срезать угол» из-за значительных трудозатрат (рутинного ручного труда) по их недопущению. Предлагаемый фреймворк позволяет избежать этого.

Цель

Цель проекта — создание фреймворка, который включает методологию разработки УМ и инструмент кодогенерации.

Задачи

  • Создать библиотеку базовой ФСТ для тактических 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).

Список источников и литературы

  1. Белов М.В., Новиков Д.А. Методология комплексной деятельности. М.: ЛЕНАНД, 2018.

  2. Белов М.В., Новиков Д.А. Модели деятельности: основы математической теории деятельности. М.: ЛЕНАНД, 2021.

  3. Николенко В.Ю. Секреты успешных НИОКР. [б.м.]: Издательские решения, 2023.

  4. Новиков А.М., Новиков Д.А. Методология. М.: Синтег, 2007.

  5. Новиков А.М., Новиков Д.А. Методология: словарь системы основных понятий. М.: Либроком, 2013.

  6. Новиков Д.А. Кибернетика: Навигатор. История кибернетики, современное состояние, перспективы развития. М.: ЛЕНАНД, 2016.

  7. Новиков Д.А. Методология управления. М.: Либроком, 2011.

  8. Новиков Д.А. Теория управления организационными системами. М.: МПСИ, 2005.

  9. Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. СПб.: Питер, 2023.

  10. Теория управления. Дополнительные главы. Авторский коллектив под ред. Новикова Д.А. М.: ЛЕНАНД, 2019.

  11. Granin. A. Functional Declarative Design: A Comprehensive Methodology for Statically-Typed Functional Programming Languages.

  12. Khononov V. Learning Domain-Driven Design. O’Reilly Media Inc, 2021.

  13. Khorikov V. Enterprise Craftsmanship.

Теги:
Хабы:
-2
Комментарии5

Публикации

Ближайшие события