Pull to refresh

Comments 19

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

По моему мнению, именно это и сделал Дядя Боб в "Чистой архитектуре". Он не создавал что-то принципиально новое, а собрал, формализовал и обобщил уже существующие идеи.

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

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

Если считаете возможным создать что-то одно для всех - дерзайте, было бы интересно почитать.

так... непорядок. Смотрю сюда.

Сравнение терминологии слоёв и компонентов архитектур

Сущности есть, объекты-значения есть. Где агрегаты? Агрегаты, которые отвечают за транзакционную целостность и согласованность всех сущностей и объектов-значений внутри себя. Где они, родимые?

Это как посмотреть) может даже Value Object был лишним для целей этой статьи. Возможно, она будет дополнена в дальнейшем, но то о чем вы говорите, это уже часть DDD. И если бы я затронул агрегаты, то не обошлось бы без описания repository, unit of work и многого другого

Есть что обсудить, например. Компоненты слоя APPLICATION

Mappers – это вспомогательные компоненты, которые преобразуют Entity в DTO и обратно.

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

Вопрос: почему маппер, который лежит в слое приложения, должен знать структуру DTO, которая определяется "нижним" слоем инфраструктуры? Или я что-то не понял?

Допустим, это речь о DTO слоя приложения, и он не совпадает в DTO слоя инфраструктуры. Это нормальная ситуация. Тогда в слое инфраструктуры должен быть маппер DTO приложения на DTO инфраструктуры, но у Вас этого нет в описании слоя инфраструктуры.

Какая-то нестыковочка получается.

Скорее всего понимается, что Entity это и есть DTO слоя инфраструктуры.

Здесь вопрос в другом - где лежат мапперы и, главное, почему?

Это важно. Если они лежатт не там где надо, то в других местах тоже возникнут проблемы.

Чтобы соблюдать Dependecy Rule нам будет достаточно иметь максимум 3 уровня маперов (это совсем не необходимо, но в теории возможно).
1. Мы мапим на уровне Domain/Application только те данные, которые не являются инфраструктурными. Т.е. по сути переводим условного UserCreateDTO в UserCreateDomain, выполняя бизнес логику use_case по хешированию пароля. У нас может даже не быть отдельного мапера, сам UserCreateDTO может иметь метод to_entity и from_entity.
2. Мы мапим на уровне Infrstructure. Пример: у нас есть сервис, который делает запрос во внешний API и получает вакансии. В зависимости от реализации сервиса, схема данных будет разной (например, получаем данные с headhunter, habr...). И сам адаптер такого сервиса будет знать, как преобразовать такую схему (которую можно считать DTO) к Entity или другой DTO, которую может принять use-case.
3. DTO существует только на уровне представления и сам мапится на use-case просто через примитивы. Мы не передаём DTO в use-case, сам use-case ничего не знает об этом DTO.

Есть один момент, который меня беспокоит, и честно у меня нет на него ответа. Мартин говорит:
"Важно, чтобы через границы передавались простые, изолированные структуры данных. Не нужно хитрить и передавать объекты сущностей или записи из базы данных".

Если выполнять это условие, то Entity не должны выходить на уровень инфраструктуры для мапинга таких схем. Придётся иметь ещё одну промежуточную DTO, которая будет способна мапиться к Entity, но существовать на уровне application, а не Insfrastrusture. Это лишняя работа, которую в принципе можно проделать, но можно просто о ней помнить.

На самом деле я согласен, что mappers могут быть почти на любом уровне. Это тоже самое, что иметь exceptions на любом уровне, или в insfrastructure видеть конкретную реализацию services.
Я не хотел сказать, что mappers только и только могут быть в application, или что DTO могут быть только в DOMAIN и нигде иначе. Мы знаем случаи, когда DTO - это InputData на уровне presentation, которая будет просто преобразована в примитивы, которые необходимы для use-case. А сам DTO будет определен именно на этом уровне и никогда не попадёт в application слой.
Примеров у меня таких не мало. Здесь скорее стоит просто понимать суть, для чего их использовать. А где и как вы будете их использовать - это уже договоренности на уровне проекта.

Это не так. Мапперы, которые отвечают за преобразование данных при переходе между слоями, должны быть в точно определённых местах нижних уровнях. Если у вас это не так, скорее все у вас нарушение зависимости слоев.

Ну давайте рассмотрим пример, а вы скажете где здесь нарушение зависимостей.
1. Мапер определен на уровне Application и умеет мапить UserCreateDTO к UserCreateDomain, который нужен для регистрации пользователя.
В Presentation через интерфейсы IPasswordHasher и IUserUnitOfWork мы подставляем реализации. Т.е. Presentation зависит от инфраструктуры. Далее на уровне use-case отрабатывает мапер и через репозиторий и получаем UserDomain, который опять же мы можем через мапер превратить в условный UserDTO и отдать на уровень представления.
Domain - знает только о Entity/DTO (если DTO определено там)
Application - знает только о Domain и DTO и предоставляет реализацию (через интерфейсы) конкретного use-case. Т.е. зависит от domain
Insfrastructure - знает только о реализации IUserUnitOfWork и IPasswordHasher, т.е. зависит от domain и application
Presentation - знает только о infrastructure и application.

2. Допустим мы запрашиваем вакансии с различных агрегаторов через celery.
У каждого агрегатора своя схема данных, которые они отдают. Можно считать её DTO. Её мы не можем использовать на уровне application и она будет определена на уровне infrastructure, вместе с реализацией самого адаптера.
В этом случае схема такая:
На уровне presentation идёт запуск задачи парсинга, этот уровень знает только о use-case'e collect_vacancies и реализациях интерфейсов, которые нужны для его выполнения.
На уровне infrastructure будет реализация адаптера, который запрашивает данные и мапит их под VacancyDomain или VacancyDTO.
На уровне application будет только use-case collect_vacancies , который определяет через интерфейсы, что именно запросить и как, он получит данные с уровня инфраструктуры в виде DTO или Entity и дальше сможет с ними сделать, что захочет (например добавить в базу).
И опять схема:
Domain - знает только о Entity/DTO (если DTO определено там)
Application - знает только о Domain и DTO и предоставляет реализацию (через интерфейсы) конкретного use-case.
Insfrastructure - знает только о реализации IVacancySourceClient и IVacancyUnitOfWork, а также имеет собственную схему данных под каждый тип адаптера, например HeadHunterAdapter будет иметь собственную схему HHVacancy, которую можно мапить к VacancyDomain/VacancyDTO. Т.е. по сути знает только о domain и application.
Presentation - знает только о infrastructure и application.

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

  1. я не думаю о домене как слое внутри слоя приложения. Здесь граница сильно размыта. Эта часть сама проявится, когда будут четко выстроены границы слоев. Для меня домен - это ограниченный контекст, далее я буду использовать его именно так.

  2. я знаю, что есть 4 слоя: приложение, инфраструктура, контроллеры и конфигурация. Приложение и конфигурация могут отсутствовать.

  3. я знаю, что приложение это главный слой, все "подстраиваются" под него, кроме конфигурации. Конфигурация - это композиция компонент.

  4. я знаю, что есть вертикальные зависимости между слоями (внутренние зависимости домена) и горизонтальные, внутри слоя между разными доменами (внешние зависимости домена). Слои и домены образуют "решетку".

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

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

  7. я знаю, что есть поток данных. Он движется вертикально по слоям внутри домена в двух направлениях: сверху - вниз и обратно. Зная, что слой приложения главный и понимая какой поток обрабатывает текущий код, я понимаю, куда ставить мапперы и фабрики. Их позиция может меняться в зависимости от задачи, но у меня всегда есть ответ на вопрос - почему они здесь.

Очень малелький пример конфигурации такого подхода описан здесь с статье "Еще раз про Di-контейнеры в golang" раздел "Применяем в реальном проекте"

Спасибо, что поделились своим видением. Думаю некоторые моменты можно было бы обсудить и раскрыть, но в любом случае чувствуется выстроенная структура, которую породили не только книги, но и практика.

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

Когда я сказал, что «mappers могут находиться почти на любом уровне», я имел в виду именно в контексте корректного соблюдения потока зависимостей. Конечно, если маппер на уровне Application начнёт напрямую зависеть от DTO, определённого в Infrastructure, это будет явным нарушением.

Я пытался показать, что маппинг допустим на разных уровнях, если границы соблюдены и поток зависимостей всегда идёт от Presentation к Domain:

  1. Регистрация пользователя:

    UserCreateDTO (Presentation) → UserCreate (Domain) → User (Domain) → UserReadDTO (Presentation).

    Маппинг выполняется в use-case, зависимости не нарушаются.

  2. Импорт вакансий из внешних API:

    Ответ API (HHVacancy DTO в Infrastructure) → маппинг в Vacancy или VacancyDTO (на уровне infrastructure) → работа через Application слой.

    Use-case не знает о структуре HHVacancy.
    Мапинг на уровне infrastructure, зависимости не нарушаются.

  3. Преобразование данных на уровне Presentation:

    DTO остаётся в Presentation, данные преобразуются в примитивы, передаваемые в use-case. Опять же, зависимости не нарушаются.

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

DTO в "Чистой архитектуре" Мартина упоминается единственный раз в качестве решения проблемы пересечения границ между слоями при необходимости соблюдения направления зависимостей в сторону бизнес-правил и сущностей. Принадлежит тому же слою, что и сценарии использования.

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

Архитектура в программировании

Перед тем как употребить 58 раз "архитектур" желательно в начале было уточнить, что речь исключительно про архитектуру ПО (приложений \ решений).

Хорошая статья.
У вас получилось "собрать в одном месте ключевые понятия и принципы"

Отличное начало! Жду продолжения. Очень хочется пощупать реальный код)

Спасибо) готовлю статью, сам проект уже написан, останется подрефакторить перед публикацией

Sign up to leave a comment.

Articles