Лучшая практика - Архитектура с несколькими слоями и DTO

После прочтения некоторых вопросов Q/As здесь, в stackoverflow, я все еще запутался в правильной реализации DTO в моем веб-приложении. Моя текущая реализация - это многоуровневая архитектура на основе Java EE (с персистентностью, сервисом и уровнем представления), но с "общим" пакетом, который используется всеми слоями, содержащими (среди прочих) объекты objecs. В этом случае слои не могут считаться независимыми. Я планирую шаг за шагом удалить общий пакет, но я сталкиваюсь с различными проблемами/вопросами:

  • Предположим, что слой persistence будет использовать класс myproject.persistence.domain.UserEntity(объект на основе JPA) для хранения и загрузки данных в/из базы данных. Чтобы показать данные в представлении, я бы предоставил еще один класс myproject.service.domain.User. Где я их конвертирую? Будет ли служба для пользователей отвечать за преобразование между двумя классами? Это действительно поможет улучшить сцепление?
  • Как должен выглядеть класс User? Должны ли они содержать только геттеры, чтобы быть неизменяемыми? Разве это не было бы громоздким для взглядов на редактирование существующих пользователей (создание нового пользователя, использование геттеров существующего объекта User и т.д.)?
  • Должен ли я использовать те же DTO-классы (Пользователь) для отправки запроса службе для изменения существующего пользователя/создания нового пользователя или для реализации других классов?
  • Не будет ли уровень представления сильно зависеть от уровня сервиса, используя все DTO в myproject.service.domain?
  • Как обрабатывать мои собственные исключения? Мой текущий подход перескакивает большинство "серьезных" исключений, пока они не обрабатываются уровнем представления (обычно они регистрируются, и пользователю сообщают, что что-то пошло не так). С одной стороны, у меня проблема, что я снова обмениваю общий пакет. С другой стороны, я до сих пор не уверен, что это можно считать "лучшей практикой". Любые идеи?

Спасибо за любые ответы.

Ответы

Ответ 1

Наличие некоторых пакетов среди разных слоев не является чем-то необычным, однако обычно это делается только для сквозных задач, таких как ведение журнала. Ваша модель не должна делиться разными слоями, или изменения в модели потребуют изменений во всех этих слоях. Как правило, ваша модель представляет собой более низкий уровень, близкий к слою данных (более, под или переплетенный, в зависимости от подхода).

Объекты передачи данных, как следует из их имени, являются простыми классами, используемыми для передачи данных. Таким образом, они обычно используются для связи между уровнями, особенно когда у вас есть архитектура SOA, которая обменивается сообщениями, а не объектами. DTO должны быть неизменными, поскольку они просто существуют для передачи информации, а не для ее изменения.

Ваши объекты домена - это одно, ваши DTO - это другое дело, а объекты, которые вам нужны в вашем слое презентации, - это еще одна вещь. Однако в небольших проектах может не стоить усилий по внедрению всех этих разных наборов и конвертации между ними. Это зависит только от ваших требований.

Вы разрабатываете веб-приложение, но оно может помочь вашему дизайну спросить себя: "Могу ли я переключить свое веб-приложение на настольное приложение? Неужели мой сервисный уровень не знает о моей логике представления?". Мышление в этих условиях поможет вам улучшить архитектуру.

На ваши вопросы:

Предположим, что слой persistence будет использовать класс myproject.persistence.domain.UserEntity(объект на основе JPA) для хранения и загрузки данных в/из базы данных. Чтобы показать данные в представлении, я бы предоставил еще один класс myproject.service.domain.User. Где я их конвертирую? Будет ли служба для пользователей отвечать за преобразование между двумя классами? Это действительно поможет улучшить сцепление?

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

Как должен выглядеть класс User? Должны ли они содержать только геттеры, чтобы быть неизменяемыми? Разве это не было бы громоздким для взглядов на редактирование существующих пользователей (создание нового пользователя, использование геттеров существующего объекта User и т.д.)?

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

Должен ли я использовать те же DTO-классы (Пользователь) для отправки запроса службе, чтобы изменить существующего пользователя/создать нового пользователя или мне нужно реализовать другие классы?

Способы обслуживания могут выражать операцию, а DTO - ее параметры, содержащие только данные. Другим вариантом является использование команд, которые представляют операцию, а также содержат DTO. Это популярно в архитектурах SOA, где ваша служба может быть простым командным процессором, например, с одной единственной операцией Execute с интерфейсом ICommand в качестве параметра (в отличие от одной операции для каждой команды).

Не будет ли уровень представления сильно зависеть от уровня сервиса, используя все DTO в myproject.service.domain?

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

Как обрабатывать мои собственные исключения? Мой текущий подход перескакивает большинство "серьезных" исключений, пока они не обрабатываются уровнем представления (обычно они регистрируются, и пользователю сообщают, что что-то пошло не так). С одной стороны, у меня проблема, что я снова обмениваю общий пакет. С другой стороны, я до сих пор не уверен, что это можно считать "лучшей практикой". Любые идеи?

Каждый слой может иметь свои собственные исключения. Они перетекают от одного уровня к другому, заключенного в следующий вид исключения. Иногда они обрабатываются одним слоем, который будет что-то делать (например, протоколирование) и, возможно, затем выдает другое исключение, которое должен обрабатывать верхний уровень. В других случаях они могут быть обработаны, и проблема может быть решена. Подумайте, например, о проблеме, связанной с базой данных. Это будет исключение. Вы могли бы справиться с этим и решили повторить попытку через секунду, и, может быть, тогда будет успех, поэтому исключение не будет течь вверх. Если повторная попытка также завершится неудачей, исключение будет повторно выбрано, и оно может полностью пройти до уровня презентации, где вы изящно уведомите пользователя и попросите его повторить слой.

Ответ 2

Свободная связь - действительно рекомендуемый способ пойти, а это значит, что вы получите огромное, скучное, чтобы писать, больно поддерживать конвертеры в вашей бизнес-логике. Да, они принадлежат к бизнес-логике: слой между DAO и представлениями. Таким образом, бизнес-уровень будет зависеть как от DTO DTO, так и от DTO. И будет полным классов конвертера, разбавляя ваше представление о реальной бизнес-логике...

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

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

Наконец, в отношении исключений. Ответственность за внешний уровень, уровень представления (контроллеры, если вы используете Spring), лежит на внешнем уровне, чтобы ловить ошибки, распространяемые из внутренних слоев, используя исключения, используя специальные поля DTO. Тогда этот внешний уровень должен решить, следует ли сообщать клиенту об ошибке и как. Дело в том, что вплоть до самого внутреннего слоя вам нужно различать различные типы ошибок, которые должен обрабатывать внешний слой. Например, если что-то происходит на уровне DAO, а на уровне представления необходимо знать, следует ли возвращать 400 или 500, слой DAO должен будет предоставить уровень представления информации, необходимой для принятия решения о том, какой из них использовать, и эта информация потребуется пройти через все промежуточные уровни, которые должны иметь возможность добавлять свои собственные ошибки и типы ошибок. Распространение исключения IOException или SQLException на внешний уровень недостаточно, внутренний слой должен также указывать внешний слой, если это ожидаемая ошибка или нет. Грустно, но это правда.