Должны ли мои исключения домена быть выброшены из уровня приложения?

Я читаю книгу Вона Вернона «Реализация дизайна, ориентированного на предметную область». Вот пример приложения для управления проектами. Есть агрегаты, такие как BacklogItem, Sprint и т. Д. Если у меня есть BacklogItemNotFoundException, определенное на уровне домена. Должен ли мой адаптер Rest перехватить его и преобразовать в NotFoundHttpResult? Или любые другие неработающие инвариантные исключения, такие как: EmailPatternBrokenException или TooManyCharactersForNameException, или что-то еще, что должно обрабатываться в адаптере Rest (архитектура портов и адаптеров) и повторно преобразовываться в ответы отдыха? Если да, значит, RestAdapter должен иметь ссылку на уровень домена? Вот что меня беспокоит ...


person DmitriBodiu    schedule 21.08.2018    source источник
comment
Этот вопрос является явным призывом к горячему, где проверить? и обсуждение исключения и простого возвращаемого значения :)   -  person guillaume31    schedule 21.08.2018


Ответы (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);      
}
}

Адаптер (например, пользовательский интерфейс) перехватит исключение приложения и покажет свое сообщение пользователю. Но он не знает об исключениях домена.

person choquero70    schedule 23.08.2018
comment
Звучит хорошо. Не могли бы вы поделиться примером кода, как выполняется это сопоставление? Вы перехватываете какое-либо исключение домена и сопоставляете его с ApplicationException, или вы создаете отдельный класс исключения для каждого исключения домена? - person DmitriBodiu; 04.09.2018
comment
@DmitriBodiu Я обновил свой ответ, объясняя, как я это делаю, и показываю код - person choquero70; 04.09.2018
comment
Большое спасибо за ваш пример! У меня есть один вопрос. Как бы вы справились со сценарием, когда ваш репозиторий выдает исключение UserNotFoundException, и вы должны сопоставить его с HttpNotFoundResult? - person DmitriBodiu; 05.09.2018
comment
@DmitriBodiu Я обрабатываю это как любое другое исключение домена. Переводчик сопоставляет UserNotFoundException с объектом ApplicationException с сообщением об исключении UserNotFoundException, прочитанном из файла свойств. Клиент (например, пользовательский интерфейс) просто выполняет команду (службу приложения), перехватывает ApplicationException и показывает сообщение пользователю. У меня нет различных типов исключений приложений, таких как HttpNotFoundResult, расширяющих AplicationException, но вы могли бы, если хотите. В переводчике вы должны спросить, является ли исключение домена UserNotFoundException, и если да, бросить HttpNotFoundResult - person choquero70; 05.09.2018

Я стараюсь избегать исключений домена, насколько это возможно, и вместо этого предпочитаю делать недостижимые состояния недоступными. Первая причина заключается в том, что исключения предназначены для исключительных, неожиданных вещей, а вторая - в том, что мне не нравится, что мой код загроможден мелкозернистыми попытками / уловками для каждой мелкой деловой вещи, которая может пойти не так.

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 |
+-----------------------+--------------------+-------------------------------------------------+
person guillaume31    schedule 21.08.2018
comment
Значит, вы не являетесь поклонником объектов значений для проверки или у вас была бы дублированная проверка в домене и на уровне пользовательского интерфейса / приложения? Я обычно дублирую логику проверки, когда использую фреймворки в пользовательском интерфейсе (отчеты о множественных ошибках и т. Д.) И исключения в домене (такие значения неожиданно достигают этой точки). - person plalx; 21.08.2018
comment
Этот ответ покрывает это. Я бы добавил, что внешние проверки удобны и должны предотвращать распространенные / типичные ошибки, но домен является истинным авторитетом, и если эти правила нарушены, они должны быть увеличены. - person Eben Roux; 22.08.2018
comment
@plalx Я обычно не дублирую, в основном потому, что в тех случаях, когда возникает вопрос, граница между валидностью пользовательского ввода и валидностью домена обычно размыта. Например, имя пользователя ‹= 100 символов является правилом домена или действительно техническим правилом? Сомневаюсь, я предпочитаю проверять только на стороне пользовательского интерфейса / приложения. Но да, я вижу, что дублирование валидации полезно там, где это оправдано. - person guillaume31; 22.08.2018
comment
предпочитают делать недостижимые состояния недоступными Что вы имели в виду - какой метод вы используете для этого? - person lonix; 26.05.2019
comment
@Ionix Я имею в виду недоступность во время выполнения, потому что она напрямую закодирована в типах. Если ваш язык позволяет, наличие системы типов участвовать в обеспечении соблюдения доменных инвариантов - хорошая идея. - person guillaume31; 27.05.2019

Я добавлю свои 2 цента за обработку ошибок, не связанных конкретно с DDD.

Исключение составляют часть контракта, который вы предоставляете потребителю. Если ожидается, что вы, например, добавите товар в корзину, исключение, которое вы можете явно выбросить, включает itemNotAvailable, shoppingCartNotExisting и т. Д.

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

Остаточный интерфейс - это контракт на операцию над ресурсом. При использовании rest over http условия контракта связаны с протоколом http.

Типичная операция, описанная выше (добавление, например, размещение элемента в ресурсе корзины), будет преобразована, например, в 404 для shoppingCartNotExisting и 409 для itemNotAvailable (конфликт, например, обновление ресурса больше невозможно, потому что какое-то состояние за это время изменилось. ).

Так что да, все исключения «домена» (ожидаемые исключения как часть контракта) должны быть явно отображены остальным адаптером, все непроверенные исключения должны приводить к ошибке 500.

person Gab    schedule 22.08.2018

TL; DR; Это нормально, если уровень приложения или презентации зависит от уровня домена, другой способ не рекомендуется.

В идеале не должно существовать никакой зависимости от одного уровня к другому, но это невозможно, иначе программное обеспечение будет непригодным для использования. Вместо этого вы должны попытаться минимизировать количество и направление зависимостей. Общее правило или лучшая практика для чистой архитектуры состоит в том, чтобы уровень домена не зависел от инфраструктуры или уровня приложения. Объекты домена (агрегаты, объекты значений и т. Д.) Не должны заботиться о конкретной персистентности, Rest, HTTP или MVC, точно так же, как эксперты домена не заботятся об этих вещах.

В реальном мире на уровень домена могут влиять технологии (например, фреймворки). Например, мы помещаем аннотации, чтобы пометить некоторые объекты домена как ведущие определенным образом при сохранении, вместо использования внешних файлов XML или JSON только потому, что они под рукой, их легче поддерживать. Однако нам необходимо свести эти влияния к минимуму.

person Constantin Galbenu    schedule 21.08.2018

Прикладной уровень сам по себе является предметной областью, специфичной для бизнеса. Таким образом, ваш уровень приложения должен обрабатывать исключение домена в зависимости от того, что ожидает приложение / бизнес. Приложение (например, клиентское веб-приложение, мобильное приложение, внутреннее приложение CRM или интерфейс API для внешнего интерфейса), вероятно, не единственный клиент уровня домена (например, rest api, библиотека jar). Могут быть определенные исключения домена, которые вы не хотите раскрывать конечному пользователю, поэтому приложение должно заключать эти исключения в оболочку или обрабатывать исключения глобально.

person alltej    schedule 21.08.2018