我的域异常应该从应用层抛出吗?

Should my Domain Exceptions be thrown from Application Layer?

我正在阅读 Vaughn Vernon 的书 - 实施领域驱动设计。有一个项目管理应用程序的示例。有像 BacklogItem、Sprint 等聚合。如果我在域层中定义了 BacklogItemNotFoundException。我的 Rest 适配器应该捕获它并转换为 NotFoundHttpResult 吗?或任何其他损坏的不变异常,如:EmailPatternBrokenException 或 TooManyCharactersForNameException 或任何应在 Rest 适配器(端口和适配器架构)中处理并重新转换为 Rest 响应的内容?如果是,是否意味着 RestAdapter 应该有对 Domain 层的引用?这就是困扰我的...

TLDR; Application 或 Presentation 层对 Domain 层有依赖就OK,反之不推荐。

理想情况下,一层到另一层不应该存在任何依赖关系,但这是不可能的,否则软件将无法使用。相反,您应该尽量减少依赖项的数量和方向。干净架构的一般规则或最佳实践是让领域层与基础设施或应用层无关。领域对象(聚合、值对象等)不应该关心特定的持久性或 Rest 或 HTTP 或 MVC,就像领域专家不关心这些事情一样。

在现实世界中,域层可能会受到技术(如框架)的影响。例如,我们放置注释来标记某些域对象在持久化时以某种特定方式表现,而不是仅仅因为它就在手边就使用外部 XML 或 JSON 文件,这样更容易维护它们。但是,我们需要将这些影响限制在最低限度。

我尽量避免域异常,而更喜欢使无效状态无法访问。第一个原因是异常是异常的、意想不到的事情,第二个原因是我不喜欢我的代码被细粒度的 try/catches 弄得乱七八糟,因为每一个可能出错的小业务。

BacklogItemNotFoundException

对我来说,这通常是您的存储库或查询服务返回 null 或空列表。不需要域例外。

EmailPatternBrokenException

TooManyCharactersForNameException

我让我的网络框架的验证功能处理这些。您也可以在域中检查它,但它很少会达到那个点,您真的不需要专门处理这种错误。

因此,典型的两种情况是:

+-----------------------+--------------------+-------------------------------------------------+
| Domain                | Application        | Presentation                                    |
+-----------------------+--------------------+-------------------------------------------------+
| Expected failure case | Return Result.Fail | Clean error message                             |
+-----------------------+--------------------+-------------------------------------------------+
| Exception             | -                  | Caught in catch-all clause > 500 error or other |
+-----------------------+--------------------+-------------------------------------------------+

应用层本身就是业务特定领域。所以你的应用层应该根据 application/business 的期望来处理域异常。应用程序(例如面向客户端的 Web 应用程序、移动设备、内部 CRM 应用程序或后端换前端 API)可能不是域层的唯一客户端(例如休息 api ,一个 jar 库)。可能存在您不想向最终用户公开的某些域异常,因此应用程序必须专门包装这些异常或全局处理异常。

我将添加关于错误处理的 2 美分,与 DDD 无关。

异常是您向消费者公开的合同的一部分。例如,如果您希望将商品添加到购物车,您可以显式抛出的异常包括 itemNotAvailable、shoppingCartNotExisting 等...

另一方面,技术异常不是合同的一部分,它们可能会发生但不应明确处理,因为没有人可以对此做任何事情,它们必须暗示操作中断(以及当前的回滚工作单元)。

rest 接口是对资源进行操作的契约。当使用 rest over http 时,合同条款与 http 协议相关。

上述典型操作(添加即 post 购物车资源上的项目)将被转换为例如 404 用于 shoppingCartNotExisting 和 409 用于 itemNotAvailable(冲突即资源更新是不可能了,因为某些状态同时发生了变化。

所以是的,所有 "domain" 异常(作为合同的一部分的预期异常)应该由其余适配器显式映射,所有未经检查的都应该导致 500 错误。

问题自相矛盾。如果是Domain Exception,说明是域抛出的。

反正领域抛出的异常应该由应用层来处理

我有一个用于命令总线的异常处理程序装饰器,它可以捕获任何域异常并将其转换为应用程序异常。

此应用程序异常被抛给适配器。

适配器知道应用程序异常,而不是域异常。

更新

我的域异常是一个抽象基础class,具体的域异常从中继承

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 有一个键和一个参数列表。消息位于 属性 文件中,其中键是具体域异常的名称 class.

应用程序异常只是一种类型,它持有具体的文本消息。

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);      
}
}

适配器(例如 UI)将捕获应用异常并将其消息显示给用户。但它不知道域异常。