Skip to content

dmpichugin/ddd-code-toolkit

Repository files navigation

DDD в действии

Руководство с примерами на Kotlin по внедрению Предметно Ориентированного Проектирования (Domain Driven Design) в команду и обращения её в безумную машину по доставке чистого кода.

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

Обсудим Сложности:

Alt text

email: maxim[at]codemonsters.team
https://t.me/codemonsterslogs

«I am strong believer in a “begin with the concrete, and move to the abstract” pedagogical approach» © Scott Wlaschin

«The problem contains the solution»

Как писать меньше кода с тестами - в кукбуке ответ.

DDD is not Done!
DDD 20 years


Вызовы:

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

Бизнес

  • Снизить стоимость доработок
  • Снизить стоимость поддержки
  • Снизить стоимость погружения новичков
  • Сокращения количества ошибок

Лид

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

  • Повышения эффективности делегирования
  • Сокращения времени на проверку кода
  • Сокращения времени на осознание кода и на встречи «погружения разработчиков» в методологию
  • Задать шаблон разработки бизнес-логики

Developer Advocate

  • Тесты - это невероятно веселый прагматичный процесс
  • DDD - это не сложно и эффективно

Задача:

Которую получилось решить

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

Концепция кукбука

OpenSource c пометками на изучение паттернов и их применение.

ShowStudio.com Nick Knignt - открыл миру процесс производства рекламного изображения.
Alt text

Jamie Oliver - открыл миру простоту приготовления высокой кухни.
Alt text


На автора оказали влияние работы инженеров:
Владимира Хорикова, Скотта Влашина, Роберта Мартина

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

DDD в действии : Рецепт

DDD :: Качественная разработка - результат качественной коммуникации, а не постановки.

Разработка - это не просто программирование.

Код - побочный эффект коммуникации

Eric Evans:

Highly productive teams grow their knowledge consciously, practicing continuous learning (Kerievsky 2003). For developers, this means improving technical knowledge, along with general domain-modeling skills (such as those in this book). But it also includes serious learning about the specific domain they are working in.

Dan North:

«Невежество — это самое большое препятствие на пути к пропускной способности»

Prof. David West:

«The amazing thing about DDD was not the patterns or the practices, it was the quiet way it put the lie to a fundamental tenets of software engineering: the lie that programmers did not need to have an understanding of domains, that everything they needed to know was a set of requirements that the code must satisfy.»

DevOps atlassian:

DevOps is an organizational culture shift that emphasizes continuous learning and continuous improvement, especially through team autonomy, fast feedback, high empathy and trust, and cross-team collaboration.

  • Разработчик - эксперт предметной области
  • Общаемся с экспертами и проектируем решения командой | Event Storming на свой лад
  • Прозрачность процессов vs Job Safety
  • Свобода ?: Уволиться одним днем

Контекст задачи

Команда и продукт

Alt text

Bounded Context

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

Контекст Задачи Обновление Абонента
Alt text

Границы ответственности сервиса Subscribers - по бизнес потокам:

Абонент

  • Сотворение абонента
  • Изменение лояльности
  • Обновление абонента ~New

Если просто Абоненты знают и умеют все про абонентов и могут обработать запрос на изменение данных абонента.

Границы ответственности сервиса обновления данных

Обновление данных

  • получает информацию из внешней системы об абоненте
  • получает информацию из внутренней системы об абоненте
  • формирует запрос на обновление данных

Общий контракт двух контекстов: запрос на обновление данных


Организация процессов в команде

Ubiquity Language

Общий язык - Единый язык в документации, коде, в общении с экспертами домена.

Alt text

Описываем тикеты, сопроводительную документацию

  • Все документы по обновлению данных лежат рядом в одном месте под рукой
  • Документация к сервисам - в исходниках
  • Сопроводительную документацию собираем в одном разделе по бизнес-процессу

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

Alt text


Опиши верхнеуровнево в функциональном стиле бизнес-процесс

Описание должно просто ответить на вопрос:
Что происходит в системе по бизнес-процессу?

Пример:

Чтобы обновить данные абонента, необходимо:

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

^ хорошо помогает в рефакторинге
может оказаться так:

  • код делает не совсем то и не совсем так, что должен или много лишнего.

Отвратительный паттерн:

Разработчик сидит и ждет дательную постановку: что и как должно происходить в каком сервисе в какой таблице.
Отвечают за все аналитики.

Так все еще бывает:

   0. Снять с себя ответственность и кодить по постановке
   1. В таблице <обновление_данных> взять все строки со статусом need_to_update
   2. В таблице абонента взять данные по абоненту по идентификатору <обновление_данных>.subscriber_id,
      если таких данных нет, пометить ошибкой
   3. Сверить строки как то так.
   4. Если данные отличаются см. пункт 5
   5. Отдельная страница в конфлюенс на два скрола со сложной логикой обновления данных, 
      сиквенс диаграммой и т.д.
  • Чем плохи долгие постановки в конфлю с кучей информации по имплементации от аналитика?
  • Аналитикой в том числе занимается разработчик как эксперт предметной области
  • Забудь Table-Driven Design (Database Oriented мышление) - используй только Доменные объекты при обсуждении задачи. Не думай о низкоуровневой реализации.

Перенесем документацию в Код

Помни:

Код - побочный эффект коммуникации

Постановка:

| запросить данные для обновления абонента
| запросить текущие данные абонента в системе
| сформировать запрос на обновление абонента
| отправить запрос обновления данных абонента

Пример кода из сервиса в стиле P.O.P.:

@Service

fun dataUpdateProcess(unvalidatedUpdateRequest: UnvalidatedDataUpdateRequest)
: Mono<Result<SubscriberDataUpdateResponse>> =
    Mono.just(ValidatedDataUpdateRequest.emerge(unvalidatedUpdateRequest))
    .flatMap { findDataWithUpdates(it) }
    .flatMap { findSubscriberForUpdate(it) }
    .flatMap { prepareSubscriberUpdateRequest(it) }
    .flatMap { updateSubscriber(it) }

Data Flow Programming

Scott Wlaschin:

Passing data through a pipeline of transformations is an alternative approach to classic OOP

Бенефиты:

  • код есть документация - просто читать не только программисту

  • это всегда однонаправленный поток, даже с ответвлениями

  • упрощает компоновку шагов

  • помогает следовать принципам хорошего дизайна

  • | Волшебство Unix Pipe Головного Мозга

 curl | jq | more
 ls -la | grep 

A pipe is a form of redirection (transfer of standard output to some other destination) that is used in Linux and other Unix-like operating systems to send the output of one command/program/process to another. А что если я разработчик и У меня windows?


Какие паттерны нам помогут описать бизнес-процесс так просто?

  • R.O.P Railway Oriented Programming in Error Handling
  • Сильная Доменная модель | Rich Domain Model
  • Type Driven Development другое TDD
  • Onion Architecture
  • TDD: Классическая школа Тестирования и совсем немного лондонского вайба

Соберем все это в чистый код в функциональной парадигме?


Я выделю две центральные идеи вокруг которых все прочно выстраивается:

Две крепости тактических паттернов в функциональной парадигме DDD

  • R.O.P Railway Oriented Programming in Error Handling
  • Сильная Доменная модель | Rich Domain Model

R.O.P - Railway Oriented Programming in Error Handling

Задача:

  • Не используем исключения в качестве control flow

Ни в модели ни в интеграционных взаимодействиях

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

Мы используем Railway Oriented Programming - error handling in functional languages как паттерн при работе с ошибками.

Scott Wlaschin: To create a robust real world application you must deal with validation, logging, network and service errors, and other annoyances. So, how do you handle all this in a clean functional way?

Alt text

Постановка:

| запросить данные для обновления абонента
| запросить текущие данные абонента в системе
| сформировать запрос на обновление абонента
| отправить запрос обновления данных абонента

Пример кода из сервиса в стиле R.O.P.:

@Service

fun dataUpdateProcess(unvalidatedUpdateRequest: UnvalidatedDataUpdateRequest)
: Mono<Result<SubscriberDataUpdateResponse>> =
    Mono.just(ValidatedDataUpdateRequest.emerge(unvalidatedUpdateRequest))
    .flatMap { findDataWithUpdates(it) }
    .flatMap { findSubscriberForUpdate(it) }
    .flatMap { prepareSubscriberUpdateRequest(it) }
    .flatMap { updateSubscriber(it) }

Решение R.O.P.

Чистая функция

in > Two Track Type : Result<Data, Error>
out > Two Track Type : Result<Data, Error>

True pure and honest function style

функция всегда возвращает ответ: Two Track Type : Result<Data, Error>
если она может «сломаться» в процессе исполнения.

Result это тип Union. (Sum Type, Discriminated Union, Either) Суть в рецепте и упрощении подхода.

Relationship to the Either monad and Kleisli composition

Пример сервиса в стиле R.O.P.:

   fun dataUpdateProcess(
    unvalidatedUpdateRequest: UnvalidatedDataUpdateRequest
   ): Mono<Result<SubscriberDataUpdateResponse>> =
    Mono.just(ValidatedDataUpdateRequest.emerge(unvalidatedUpdateRequest))
        .flatMap { findDataWithUpdates(it) }
        .flatMap { findSubscriberForUpdate(it) }
        .flatMap { prepareSubscriberUpdateRequest(it) }
        .flatMap { updateSubscriber(it) }          
       ...
       ...

    // > Result IN > Result OUT
    private fun prepareSubscriberUpdateRequest(
        subscriberDataUpdate: Result<SubscriberDataUpdate>
    ): Mono<Result<SubscriberUpdateRequest>> =
    subscriberDataUpdate.fold(
        onSuccess = { it.prepareUpdateRequest() },
                      //^ Бизнес-логика в Доменном классе
        onFailure = { Result.failure(it) }
                     //^ ошибку пробрасываем далее по пайпу процесса
    ).toMono()    

Fold In functional programming, fold (also termed reduce, accumulate, aggregate, compress, or inject)

Can Execute/ Execute pattern
imperative style

private fun prepareSubscriberUpdateRequest(
    subscriberDataUpdate: Result<SubscriberDataUpdate>
): Result<SubscriberUpdateRequest> {
  if(subscriberDataUpdate.isSuccess) {
      return subscriberDataUpdate.data.prepareUpdateRequest()
  } else {
      Result.failure(subscriberDataUpdate.error)
  }      
} 

Filter Style in pipe

private fun prepareSubscriberUpdateRequest(
    subscriberDataUpdate: Result<SubscriberDataUpdate>
): Mono<Result<SubscriberUpdateRequest>> =
     Mono.just(subscriberDataUpdate)
         .filter{it.isSuccess}
         .map {it.getOrThrow()}
         .map{it.prepareUpdateRequest()}
         .switchIfEmpty(throwErrorFronInput(it))

Вывод:

  • Не используем исключения в качестве control flow

    С two track type Result<Data, Error> обработка ошибок становится гражданином первого класса нашей модели

  • исключения для нас исключительно сигналы багов!
  • чистые функции

Сильная Доменная модель | Rich Domain Model

Логика описана в доменных классах, не в сервисах:

//AggregateRoot
data class SubscriberDataUpdate private constructor(
    private val dataUpdate: DataUpdate,
    private val subscriber: Subscriber
) {

    fun prepareUpdateRequest(): Result<SubscriberUpdateRequest> =
        when (isUpdateRequired()) {
            true -> createSubscriberUpdateRequest()
            else -> failNoUpdateRequired()
        }

    private fun isUpdateRequired(): Boolean =
        subscriber.mobileRegionId != dataUpdate.mobileRegionId

    private fun failNoUpdateRequired(): Result<SubscriberUpdateRequest> =
        Result.failure(RuntimeException("No Update Required"))

    private fun createSubscriberUpdateRequest()
            : Result<SubscriberUpdateRequest> =
        Result.success(
            SubscriberUpdateRequest(
                subscriberId.value,
                dataUpdate.msisdn.value,
                dataUpdate.mobileRegionId.value,
                this
            )
        )
}

Бенефиты:

  • бизнес-логика собрана в одном месте Domain Layer
  • направляет нас на подконтрольное создание, проверку и управление сущностью, предотвращая появление у клиента сущностей с несогласованным состоянием сразу в одном месте
  • код превращается в документацию, которую просто тестировать

Не используй анти-паттерн Слабая Доменная Модель

      data class Subscriber(
            val subscriberId: String,
            val msisdn: String, 
            val mobileRegionId: String
      )
  

Чем плоха слабая доменная модель?

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

чем плохо слияние с интеграцией?

Мы описали класс сильной доменной моделью, протестируем его?

internal class SubscriberDataUpdateTest {

    @Test
    fun success() {
        //arrange
        val foundDataUpdateDto = DataUpdateDto(
            dataUpdateId = "101",
            subscriberId = "888",
            msisdn = "3338887770",
            mobileRegionId = "9" //<
        )
        val foundSubscriberDto = SubscriberDto(
            subscriberId = "888",
            msisdn = "3338887770",
            mobileRegionId = "0" //<
        )

        val dataUpdate = DataUpdate.emerge(
            foundDataUpdateDto
        ).getOrThrow()
        val subscriberResult = Subscriber.emerge(
            foundSubscriberDto
        )
        //^ воссоздаем необходимое нам состояние Обновление абонента
        //act
        val sut = SubscriberDataUpdate.emerge(dataUpdate, subscriberResult)
        //assert
        //i like fluent assertion library assertJ
        assertThat(sut.isSuccess).isTrue
        assertThat(sut.getOrThrow().prepareUpdateRequest().isSuccess).isTrue
        val subscriberUpdateRequest = sut.getOrThrow()
            .prepareUpdateRequest().getOrThrow()
        assertThat(subscriberUpdateRequest.subscriberId).isEqualTo("888")
        assertThat(subscriberUpdateRequest.msisdn).isEqualTo("3338887770")
        assertThat(subscriberUpdateRequest.mobileRegionId).isEqualTo("9")
    }
    
    //fails in da src

}

DDD :: Aggregate

Eric Avans:

An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes.

Scott Wlaschin:

An aggregate plays an important role when data is updated. The aggregate acts as the consistency boundary: when one part of the aggregate is updated, other parts might also need to be updated to ensure consistency.

The aggregate is also where any invariants are enforced.

//AggregateRoot
data class SubscriberDataUpdate private constructor(
    private val dataUpdate: DataUpdate,
    private val subscriber: Subscriber
) {
    fun prepareUpdateRequest(): Result<SubscriberUpdateRequest> = {..}
    private fun isUpdateRequired(): Boolean = {..}
    private fun failNoUpdateRequired(): Result<SubscriberUpdateRequest> = {..}
    private fun createSubscriberUpdateRequest(): Result<SubscriberUpdateRequest> = {..}
}

TDD :: Type Driven Development как защита от багов на уровне компиляции

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

Реализация Тактического Паттерна DDD: ValueObject

ValueObject - Основной кирпичик описания модели - это важно понимать. Помогает строить всегда валидную доменную модель!

Пример SubscriberId:

data class SubscriberId
private constructor(
    override val value: String
) : ValueObject<String> {
    companion object {
        fun emerge(subscriberId: String)
                : Result<SubscriberId> =
            when (isStringConsists6Digits(subscriberId)) {
                true -> Result.success(SubscriberId(subscriberId))
                else -> Result.failure(IllegalArgumentException("..."))
            }

        private val isStringConsist6Digits = "^\\d{1,6}\$".toRegex()

        private fun isStringConsists6Digits(value: String) =
            isStringConsist6Digits.matches(value)
    }
} 

Представили Тест на эту логику?

internal class SubscriberIdTest {
  @Test
  fun success() {
    val sut = SubscriberId.emerge("888")
    assertThat(sut.isSuccess).isTrue
    assertThat(sut.getOrThrow().value).isEqualTo("888")
  }

  @Test
  fun successWith6Digits() {
    val sut = SubscriberId.emerge("123456")
    assertThat(sut.isSuccess).isTrue
    assertThat(sut.getOrThrow().value).isEqualTo("123456")
  }

  @Test
  fun failWithWrongFormat() {
    val sut = SubscriberId.emerge("L124S")
    assertThat(sut.isFailure).isTrue
    assertThat(sut.exceptionOrNull()!!.message).isEqualTo("Subscriber Id consists of numbers maximum length 6")
  }

}

Всегда валидная Последовательность алгебраических типов

DDD made functional

Для описания Доменных классов в функциональном стиле помогает
описать бизнес-процесс в цепочке перетекающих классов друг в друга:

Постановка:

| запросить данные для обновления абонента
| запросить текущие данные абонента в системе
| сформировать запрос на обновление абонента
| отправить запрос обновления данных абонента

Последовательность алгебраических типов:

| Непроверенный Запрос на Обновление | UnvalidatedDataUpdateRequest
| Проверенный Запрос На Обновление   | ValidatedDataUpdateRequest
| Запрос Абонента В Системе          | SubscriberDataUpdate
| Запрос На Обновление Абонента      | SubscriberUpdateRequest
| Результат Обновления Абонента      | SubscriberDataUpdateResponse

Пример плохого возможно Невалидного Доменного класса:

   class SubscriberDataUpdate(
           val subscriber: Subscriber?, 
           val dataUpdate: SubscriberDataUpdate
     ) {
           fun isValid() = null != subscriber

           fun isUpdateRequired() = 
              subscriber.mobileRegionId != dataUpdate.mobileRegionId 
   }

Всегда валидная Доменная модель возникает только благодаря фабричным методам,
или не возникает вовсе:

data class SubscriberDataUpdate private constructor(
    private val dataUpdate: DataUpdate,
    private val subscriber: Subscriber
) {
    fun prepareUpdateRequest(): Result<SubscriberUpdateRequest> = {..}

    private fun failNoUpdateRequired(): Result<SubscriberUpdateRequest> = {..}

    private fun createSubscriberUpdateRequest()
            : Result<SubscriberUpdateRequest> = {..}
        
    private fun isUpdateRequired(): Boolean = {..}
    
    companion object {
        fun emerge(
            dataUpdate: DataUpdate,
            subscriberResult: Result<Subscriber>
        ): Result<SubscriberDataUpdate> =
            subscriberResult.map {
                SubscriberDataUpdate(dataUpdate, it)
            }
    }
    
}
  • Остановись на валидации Запроса

| Непроверенный Запрос на Обновление | UnvalidatedDataUpdateRequest

DTO -> UnvalidatedDataUpdateRequest(a: String, b:String, c: String, d: String)

Result.zip(ValueObject, ValueObject, ValueObject, ValueObject).map{..}

fun <A : Any, B : Any, C : Any, D : Any> Result.Companion.zip(a: Result<A>, b: Result<B>, c: Result<C>, d: Result<D>)
: Result<Tuple4<A, B, C, D>> =
    if (sequenceOf(a, b, c, d).none { it.isFailure })
        Result.success(Tuples.of(a.getOrThrow(), b.getOrThrow(), c.getOrThrow(), d.getOrThrow()))
    else
        Result.failure(sequenceOf(a, b, c, d).first { it.isFailure }.exceptionOrNull()!!)

Onion Architecture : Изолируем доменную модель от интеграций

Onion Architecture

Уровень сервисов используем как простой поток dump pipe

Как это помогает в тестописи?

Alt text

Постановка:

| запросить данные для обновления абонента
| запросить текущие данные абонента в системе
| сформировать запрос на обновление абонента
| отправить запрос обновления данных абонента

Пример сервиса с Сильной Доменной Моделью:

    fun dataUpdateProcess(unvalidatedUpdateRequest: UnvalidatedDataUpdateRequest)
            : Mono<Result<SubscriberDataUpdateResponse>> =
        Mono.just(ValidatedDataUpdateRequest.emerge(unvalidatedUpdateRequest))
            .flatMap { findDataWithUpdates(it) }
            .flatMap { findSubscriberForUpdate(it) }
            .flatMap { prepareSubscriberUpdateRequest(it) }
            .flatMap { updateSubscriber(it) }

     private fun findSubscriberForUpdate(dataUpdate: Result<DataUpdate>)
            : Mono<Result<SubscriberDataUpdate>> =
        dataUpdate.fold(
            onSuccess = { subscriberRequest -> findSubscriberByRest(subscriberRequest) },
            onFailure = { error -> Mono.just(Result.failure(error)) }
        )

     private fun findSubscriberByRest(dataUpdate: DataUpdate)
        : Mono<Result<SubscriberDataUpdate>> =
        _subscribersClient.findSubscriber(dataUpdate.subscriberId)
        .map { SubscriberDataUpdate.emerge(dataUpdate, it) }

YAGNI + KISS самые ценные принципы

YAGNI + KISS как самые ценные принципы проектирования

YAGNI — "You aren’t gonna need it"
KISS — "Keep it simple, stupid" or "Keep it short and simple"

  • проектируем только то, что нужно в моменте : может вообще не взлететь
  • Улучшай структуру кода и уменьшай количество слоев
    Простая структура уменьшает когнитивную нагрузку, упрощает работу с кодом.

Vertical Slice Architecture

    ddd.toolkit
      controller
      domain
        common
        subscriberDataUpdate
      utils
    
  • никаких универсальных надстроек и шаблонов. Домен уникален сам по себе
    The simpler your solution is, the better you are as a software developer.
         ddd.toolkit
           controller
           domain
             common
             subscriberDataUpdate
                DataUpdate
                Subscriber
                SubscriberDataUpdate
                SubscriberDataUpdateRequest
                SubscriberDataUpdateResponse
                SubscriberDataUpdateService
                SubscriberGateway
                SubscriberRestClient
           utils
         

TDD :: классическая школа

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

TDD — "Test Driven Development"
TDD — это надежный способ проектирования программных компонентов.
Тесты помогают писать код лучше, если поставить задачу:

  • Покрой юнит-тестами бизнес-логику, которая содержится в Доменной Модели
  • тест - это документация - должен быть максимально простым:
 /**
 4 аспекта хороших юнит-тестов:
  1) защита от багов
  2) устойчивость к рефакторингу
  3) быстрая обратная связь
  4) простота поддержки\
 **/
 @Test
 fun success() {
   //arrange
   val foundDataUpdateDto = DataUpdateDto(
       dataUpdateId = "101",
       subscriberId = "909",
       msisdn = "9999999999",
       mobileRegionId = "9" //< изменение региона
   )
   val foundSubscriberDto = SubscriberDto(
       subscriberId = "909",
       msisdn = "9999999999",
       mobileRegionId = "0" //< текущее состояние региона
   )

   val dataUpdate = DataUpdate.emerge(
       Result.success(foundDataUpdateDto)
   ).getOrThrow()
   val subscriberResult = Subscriber.emerge(
       Result.success(foundSubscriberDto)
   )   //^ моки не нужны
        
   //act
   val sut = SubscriberDataUpdate.emerge(dataUpdate, subscriberResult)
    // ^ SUT - sysyem under test
        
   //assert
   assertThat(sut.isSuccess).isTrue
   assertThat(sut.getOrThrow()
                 .prepareUpdateRequest().isSuccess
              )
              .isTrue
   val subscriberUpdateRequest = sut.getOrThrow()
                                    .prepareUpdateRequest()
                                    .getOrThrow();
   assertThat(subscriberUpdateRequest.subscriberId).isEqualTo("909")
   assertThat(subscriberUpdateRequest.msisdn).isEqualTo("9999999999")
   assertThat(subscriberUpdateRequest.mobileRegionId).isEqualTo("9")
 }

Вывод:

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

Один тест покрывает максимум возможных интеграций – максимум кода.
Проверь «Счастливый путь» и до 3-х крайних точек с ошибками по процессу
Как правило интеграционного теста на одну ошибку хватает.
Все ошибки тестируем юнит-тестами.

Пример интеграционного теста:

class SubscriberDataUpdateControllerTest(
         @Autowired val webTestClient: WebTestClient
) {
...
   
@Test
fun updateSuccess() {
  webTestClient.put()
      .uri("/api/v1/subscriber-data-updates")
      .bodyValue(RestRequest(DataUpdateRequestDto(dataUpdateId = "101")))
      .exchange()
      .expectStatus().isOk
      .expectBody()
      .jsonPath("@.actualTimestamp").isNotEmpty
      .jsonPath("@.status").isEqualTo("success")
      .jsonPath("@.data.subscriberId").isEqualTo("999")
      .jsonPath("@.data.dataUpdateId").isEqualTo("101")
}
}
 

При таком подходе к дизайну кода мы получаем из коробки качественное покрытие тестами

Alt text

Этот подход прекрасно вписывается в строительство пирамиды в команде:

Alt text

Что пишут QA в нашем случае и как это может помочь при рефакторинге?

Что дальше?

  • BDD

Вывод

DDD is good and simple not simple!

Обсудим сложности

  • принять лидерство
  • не говорить про DDD
  • внедрение в существующей команде
  • рефакторинг "старого кода" по кукбуку

Рецепт:

  • Стань экспертом предметной области – разберись что и как должно работать на всех уровнях.
    Твой код – твоя ответственность.
    И помни:
    Качественная разработка – это результат качественной коммуникации.
  • Опиши в функциональном стиле бизнес-процесс с доменными классами
  • Реализуй в функциональном стиле всегда валидную Богатую Доменную Модель без примитивов
  • Покрой юнит-тестами бизнес-логику, которая содержится в Доменной Модели
  • Запусти Доменную Модель по тоннелю «бизнес-процесс» без исключений, на шлюзах поможет two track type Result<Data, Error> и canExecute/execute.

По рецепту возможно получить в качестве результата:

  • Простую и строгую структуру приложения - хороший дизайн кода в функциональным стиле
  • Код будет оснащен эффективным набором простых юнит-тестов:
    • которые сфокусированы на изолированной от интеграций бизнес-логике
    • Количество интеграционных тестов сведено к достаточному минимуму
      • Интеграционные тесты более дорогие в сопровождении и поддержке
    • Моки не используются вообще или в крайне исключительных ситуациях

перед пушем запускай:

./gradlew test

Alt text

The best code is the one that has never been written © Vladimir Khorikov

This is The Way © The Mandalorian


Оставьте, пожалуйста, отзыв о выступлении!

https://t.ly/uf7j

Alt text


Книги:

Alt text

Alt text

Alt text

This Is Tha Way

Agile Manifesto

Полезные ссылки