Лучшая практика - многоуровневая архитектура и DTO
После прочтения некоторых вопросов и ответов здесь по stackru, я все еще не понимаю правильную реализацию DTO в моем веб-приложении. Моя текущая реализация - это многоуровневая архитектура (на основе Java EE) (с постоянством, сервисом и уровнем представления), но с "общим" пакетом, используемым всеми уровнями, который содержит (среди прочего) предметные области. В этом случае слои не могут рассматриваться как независимые. Я планирую удалить общий пакет шаг за шагом, но я сталкиваюсь с различными проблемами / вопросами:
- Предположим, что уровень персистентности будет использовать класс myproject.persistence.domain.UserEntity (сущность на основе JPA) для хранения и загрузки данных в / из базы данных. Чтобы показать данные в представлении, я бы предоставил другой класс myproject.service.domain.User. Куда мне их конвертировать? Будет ли служба для пользователей отвечать за преобразование между двумя классами? Это действительно поможет улучшить сцепление?
- Как должен выглядеть класс User? Должен ли он содержать только геттеры, чтобы быть неизменным? Разве не было бы обременительно для представлений редактировать существующих пользователей (создавать нового пользователя, использовать методы получения существующего объекта пользователя и т. Д.)?
- Должен ли я использовать те же DTO-классы (Пользователь), чтобы отправить запрос в службу для изменения существующего пользователя / создания нового пользователя, или я должен реализовать другие классы?
- Разве уровень представления не будет сильно зависеть от уровня обслуживания при использовании всех DTO в myproject.service.domain?
- Как обрабатывать мои собственные исключения? Мой текущий подход отбрасывает большинство "серьезных" исключений до тех пор, пока они не будут обработаны уровнем представления (обычно они регистрируются, и пользователю сообщают, что что-то пошло не так). С одной стороны, у меня проблема в том, что у меня снова есть общий пакет. С другой стороны, я до сих пор не уверен, что это можно считать "лучшей практикой". Есть идеи?
Спасибо за любые ответы.
2 ответа
Наличие некоторых пакетов между различными слоями не является редкостью, однако это обычно делается только для сквозных задач, таких как регистрация. Ваша модель не должна быть общей для разных слоев, иначе для изменения модели потребуются изменения во всех этих слоях. Как правило, ваша модель представляет собой нижний уровень, близкий к уровню данных (выше, ниже или переплетенный, в зависимости от подхода).
Объекты передачи данных, как следует из их названия, являются простыми классами, используемыми для передачи данных. Как таковые, они обычно используются для связи между уровнями, особенно когда у вас есть архитектура SOA, которая взаимодействует через сообщения, а не объекты. DTO должны быть неизменными, поскольку они просто существуют с целью передачи информации, а не ее изменения.
Ваши доменные объекты - это одно, ваши DTO- это другое, а объекты, которые вам нужны на уровне представления, - это другое. Однако в небольших проектах может не стоить усилий для реализации всех этих различных наборов и преобразования между ними. Это зависит только от ваших требований.
Вы разрабатываете веб-приложение, но оно может помочь вашему дизайну задать себе вопрос: "Могу ли я переключить свое веб-приложение на настольное приложение? Действительно ли мой уровень обслуживания не знает о моей логике представления?". Мышление в этих терминах приведет вас к лучшей архитектуре.
На ваши вопросы:
Предположим, что уровень персистентности будет использовать класс myproject.persistence.domain.UserEntity (сущность на основе JPA) для хранения и загрузки данных в / из базы данных. Чтобы показать данные в представлении, я бы предоставил другой класс myproject.service.domain.User. Куда мне их конвертировать? Будет ли служба для пользователей отвечать за преобразование между двумя классами? Это действительно поможет улучшить сцепление?
Уровень обслуживания знает свои классы (DTO) и уровень под ним (скажем, постоянство). Так что да, служба отвечает за перевод между постоянством и собой.
Как должен выглядеть класс User? Должен ли он содержать только геттеры, чтобы быть неизменным? Разве не было бы обременительно для представлений редактировать существующих пользователей (создавать нового пользователя, использовать методы получения существующего объекта пользователя и т. Д.)?
Идея DTO заключается в том, что вы используете их только для передачи, поэтому такие операции, как создание нового пользователя, не требуются. Для этого вам нужны разные предметы.
Должен ли я использовать те же DTO-классы (Пользователь), чтобы отправить запрос в службу для изменения существующего пользователя / создания нового пользователя, или я должен реализовать другие классы?
Методы обслуживания могут выражать операцию, а DTO- это ее параметры, содержащие только данные. Другой вариант - использовать команды, которые представляют операцию, а также содержат DTO. Это популярно в SOA-архитектурах, где ваш сервис может быть простым командным процессором, например, с одним Execute
операция взятия ICommand
интерфейс в качестве параметра (в отличие от одной операции на команду).
Разве уровень представления не будет сильно зависеть от уровня обслуживания при использовании всех DTO в myproject.service.domain?
Да, уровень над уровнем сервиса будет зависеть от него. Это идея. Положительным моментом является то, что от него зависит только тот уровень, а не верхний или нижний уровни, поэтому изменения влияют только на этот слой (в отличие от того, что происходит, если вы используете классы домена из каждого слоя).
Как обрабатывать мои собственные исключения? Мой текущий подход отбрасывает большинство "серьезных" исключений до тех пор, пока они не будут обработаны уровнем представления (обычно они регистрируются, и пользователю сообщают, что что-то пошло не так). С одной стороны, у меня проблема в том, что у меня снова есть общий пакет. С другой стороны, я до сих пор не уверен, что это можно считать "лучшей практикой". Есть идеи?
Каждый слой может иметь свои исключения. Они перетекают из одного слоя в другой, инкапсулированные в следующее исключение. Иногда они будут обрабатываться одним слоем, который будет что-то делать (например, записывать в журнал), а затем может выдавать другое исключение, которое должен обрабатывать верхний уровень. В других случаях они могут быть обработаны, и проблема может быть решена. Подумайте, например, о проблеме с подключением к базе данных. Было бы исключение. Вы можете справиться с этим и решить повторить попытку через секунду, и, возможно, тогда произойдет успех, поэтому исключение не будет течь вверх. Если повторная попытка также завершится неудачей, исключение будет сгенерировано повторно, и оно может перетекать вплоть до уровня представления, где вы изящно уведомляете пользователя и просите его повторить уровень.
Слабое связывание - действительно рекомендуемый путь, а это значит, что в итоге у вас будет огромное, скучное написание, болезненное обслуживание конвертеров в вашей бизнес-логике. Да, они принадлежат бизнес-логике: слой между DAO и представлениями. Таким образом, бизнес-уровень будет зависеть как от DTO DTO, так и от DTO. И будет полон конвертерных классов, разбавляя ваше представление о реальной бизнес-логике...
Если вы можете избежать постоянных просмотров DTO, это здорово. Для библиотеки, которую вы используете для их сериализации, может потребоваться, чтобы они имели сеттеры. Или вы можете найти их легче построить, если у них есть сеттеры.
Я прекрасно справился с использованием одних и тех же классов DTO как для представлений, так и для DAO. Это плохо, но, честно говоря, у меня не было ощущения, что система была бы в большей степени отделена, поскольку бизнес-логика, самая важная часть, должна все равно зависеть от всего. Эта тесная связь обеспечила большую краткость и упростила синхронизацию уровней представления и DAO. Я все еще мог иметь вещи, специфичные только для одного из слоев и не замеченные в другом, используя композицию.
Наконец, об исключениях. Ответственность за обнаружение ошибок, распространяемых из внутренних слоев, будь то с помощью исключений, будь то с помощью специальных полей DTO, лежит на самом внешнем слое, слое представления (контроллеры, если вы используете Spring). Затем этот внешний уровень должен решить, сообщать ли клиенту об ошибке и как. Дело в том, что вплоть до самого внутреннего слоя вам нужно различать различные типы ошибок, которые должны обрабатывать самые внешние слои. Например, если что-то происходит на уровне DAO, и уровень представления должен знать, возвращать ли 400 или 500, уровень DAO должен будет обеспечить уровень представления информацией, необходимой, чтобы решить, какой использовать, и эта информация потребуется пройти через все промежуточные уровни, которые должны иметь возможность добавлять свои ошибки и типы ошибок. Распространение IOException или SQLException на самый внешний уровень недостаточно, внутренний уровень должен также сообщить внешнему уровню, является ли это ожидаемой ошибкой или нет. Грустно, но правда.