Должны ли мои исключения домена быть выброшены из уровня приложений?
Я читаю Книгу Вона Вернона - Реализация доменного дизайна. Есть пример приложения для управления проектами. Существуют агрегаты, такие как BacklogItem, Sprint и т. Д. Если у меня есть BacklogItemNotFoundException, определенный на уровне домена. Должен ли мой адаптер Rest перехватить его и преобразовать в NotFoundHttpResult? Или любые другие неработающие инвариантные исключения, такие как: EmailPatternBrokenException или TooManyCharactersForNameException, или что-либо еще должно обрабатываться в адаптере Rest (архитектура портов и адаптеров) и преобразовываться в ответы остальных? Если да, это означает, что RestAdapter должен иметь ссылку на слой домена? Это то, что беспокоит меня...
5 ответов
Вопрос в противоречии. Если это исключение домена, это означает, что оно выбрасывается доменом.
В любом случае исключения, создаваемые доменом, должны обрабатываться прикладным уровнем.
У меня есть декоратор обработчика исключений для командной шины, который перехватывает любое исключение домена и переводит его в исключение приложения.
Это исключение приложения выбрасывается в адаптеры.
Адаптеры знают об исключениях приложения, а не об исключениях домена.
ОБНОВИТЬ
Мое исключение домена является абстрактным базовым классом, от которого наследуются исключения конкретного домена
public abstract class DomainException extends RuntimeException {
private static final long serialVersionUID = 1L;
private ErrorMessage mainErrorMessage;
private List<ErrorMessage> detailErrorMessages;
protected DomainException ( List<ErrorMessage> aDetailMessages, Object... aMainMessageArgs ) {
this.mainErrorMessage = new ErrorMessage(this.getClass().getSimpleName(), aMainMessageArgs );
this.detailErrorMessages = ( (aDetailMessages==null) ? new ArrayList<ErrorMessage>() : aDetailMessages );
}
public ErrorMessage mainErrorMessage() {
return this.mainErrorMessage;
}
public List<ErrorMessage> detailErrorMessages() {
return this.detailErrorMessages;
}
}
ErrorMessage имеет ключ и список аргументов. Сообщения находятся в файле свойств, где ключом является имя конкретного класса исключений домена.
Исключением для приложения является только один тип, который содержит конкретное текстовое сообщение.
public class ApplicationException extends Exception {
private static final long serialVersionUID = 1L;
private String mainMessage;
private String[] detailMessages = new String[0];
public ApplicationException ( String aMainMessage, Throwable aCause, String... aDetailMessages ) {
super ("Main Message = "+aMainMessage+" - DetailMessages = "+Utils.toString(aDetailMessages), aCause );
this.mainMessage = aMainMessage;
this.detailMessages = ( (aDetailMessages==null) ? (new String[0]) : aDetailMessages );
}
public String mainMessage() {
return this.mainMessage;
}
public boolean hasDetailMessages() {
return (this.detailMessages.length > 0);
}
public String[] detailMessages() {
return this.detailMessages;
}
}
У меня есть декоратор (оборачивает выполнение каждой команды) для обработки исключений домена:
public class DomainExceptionHandlerDecorator extends Decorator {
private final DomainExceptionHandler domainExceptionHandler;
public DomainExceptionHandlerDecorator (DomainExceptionHandler domainExceptionHandler) {
this.domainExceptionHandler = domainExceptionHandler;
}
@Override
public <C extends Command> void decorateCommand(Mediator mediator, C command) throws ApplicationException {
try {
mediator.executeCommand(command);
} catch ( DomainException de ) {
this.domainExceptionHandler.handle (de);
}
}
}
И у меня есть обработчик исключений домена, который принимает исключение домена, преобразует его в исключение приложения, читая файл свойств (TextMessageService выполняет эту работу) и генерирует исключение приложения.
public class TranslatorDomainExceptionHandler implements DomainExceptionHandler {
private final TextMessageService configurationService;
public TranslatorDomainExceptionHandler ( TextMessageService aConfigurationService ) {
this.configurationService = aConfigurationService;
}
@Override
public void handle ( DomainException de ) throws ApplicationException {
ErrorMessage mainErrorMessage = de.mainErrorMessage();
List<ErrorMessage> detailErrorMessages = de.detailErrorMessages();
String mainMessage = this.configurationService.mensajeDeError ( mainErrorMessage );
String[] detailMessages = new String [ detailErrorMessages.size() ];
int i = 0;
for ( ErrorMessage aDetailErrorMessage : detailErrorMessages ) {
detailMessages[i] = this.configurationService.mensajeDeError ( aDetailErrorMessage );
i++;
}
throw new ApplicationException ( mainMessage, de, detailMessages);
}
}
Адаптер (например, пользовательский интерфейс) перехватит исключение приложения и покажет его сообщение пользователю. Но он не знает об исключениях домена.
Я стараюсь максимально избегать исключений из доменов и предпочитаю вместо этого делать недействительные состояния недостижимыми. Первая причина заключается в том, что исключения относятся к исключительным, неожиданным вещам, во-вторых, мне не нравится, чтобы мой код был загроможден мелкозернистой попыткой / ловлей для каждой мелочи, которая может пойти не так.
BacklogItemNotFoundException
Для меня это обычно ваш репозиторий или служба запросов, возвращающая ноль или пустой список. Нет необходимости в исключении домена.
EmailPatternBrokenException
TooManyCharactersForNameException
Я позволил функции проверки моего веб-фреймворка справиться с этим. Вы также можете проверить это в Домене, но он редко достигнет этой точки, и вам действительно не нужно специально обрабатывать такого рода ошибки.
В результате два типичных сценария:
+-----------------------+--------------------+-------------------------------------------------+
| Domain | Application | Presentation |
+-----------------------+--------------------+-------------------------------------------------+
| Expected failure case | Return Result.Fail | Clean error message |
+-----------------------+--------------------+-------------------------------------------------+
| Exception | - | Caught in catch-all clause > 500 error or other |
+-----------------------+--------------------+-------------------------------------------------+
Я добавлю свои 2 цента выше общей обработки ошибок, а не специально к DDD.
Исключение составляют части договора, которые вы выставляете потребителю. Если вы ожидаете, например, добавить товар в корзину, исключение, которое вы можете явно сгенерировать, включает itemNotAvailable, shoppingCartNotExisting и т. Д...
Технические исключения, с другой стороны, не являются частью контракта, они могут возникать, но их не следует явно обрабатывать, поскольку никто ничего не может с этим поделать, они должны подразумевать прерывание операции (и откат текущей единицы работы).
Интерфейс отдыха - это контракт на операцию над ресурсом. При использовании rest over http условия договора связаны с протоколом http.
Типичная операция, описанная выше (добавление, т. Е. Публикация элемента в ресурсе корзины), будет переведена, например, на 404 для shoppingCartNotExisting и 409 для itemNotAvailable (конфликт, т. Е. Обновление ресурса больше невозможно, поскольку какое-то состояние изменилось между тем).
Так что да, все "доменные" исключения (ожидаемые исключения как часть контракта) должны быть явно отображены оставшимся адаптером, все непроверенные должны привести к ошибке 500.
TLDR; Это нормально, если уровень Application или Presentation имеет зависимость от уровня Domain, другой способ не рекомендуется.
В идеале не должно быть никакой зависимости от одного уровня к другому, но это невозможно, иначе программное обеспечение не будет пригодно для использования. Вместо этого вы должны попытаться свести к минимуму количество и направление зависимостей. Общее правило или наилучшая практика для чистой архитектуры - сохранять уровень домена независимым от инфраструктуры или уровня приложений. Объекты домена (агрегаты, объекты значений и т. Д.) Не должны заботиться об определенной персистентности, или Rest, или HTTP или MVC, точно так же, как эксперты домена не заботятся об этих вещах.
В реальном мире уровень домена может зависеть от технологий (таких как фреймворки). Например, мы помещаем аннотации, чтобы пометить некоторые объекты Домена как поведение определенным образом, когда они сохраняются, вместо того, чтобы использовать внешние файлы XML или JSON только потому, что они под рукой, их легче поддерживать. Однако нам необходимо ограничить это влияние до минимума.
Прикладной уровень сам по себе является бизнес-специфическим доменом. Таким образом, ваш прикладной уровень должен обрабатывать исключение домена в зависимости от того, что ожидает приложение / бизнес. Приложение (например, клиентское веб-приложение, мобильное приложение, внутреннее приложение CRM или API backend-for-frontend), вероятно, не единственный клиент уровня домена (например, остальные API, библиотека jar). Могут быть определенные исключения домена, которые вы не хотите показывать конечному пользователю, поэтому приложение должно специально обернуть эти исключения или обрабатывать исключения глобально.