存储库是否应该引发域错误

问题描述

我正在构建一个尝试遵守简洁架构的应用程序。我知道存储库旨在抽象出持久层并根据域语言返回实体。但是,这是否意味着它也应该在出现问题时检查并抛出域错误。让我们考虑一种情况,我想通过用户存储库添加用户。我可以执行以下操作:

// in user repo
const add = (user: User): void => {
  try {
    // do some database stuff
  } catch() {
    throw new EmailAlreadyInUse(user.email);
  }
}

但是这种实现是否可取?我们现在依赖于使用正确的唯一键模式正确设置的数据库来强制执行域规则(没有两个用户可以使用同一电子邮件注册)。在我看来,这似乎是我们可能会将域规则溢出到持久层。

改为从用例层抛出这个异常是否更有意义。

const AddNewUserUseCase = (userRepository,email) => {
  const user = userRepository.findByEmail(email);
  if(user) {
    throw new EmailAlreadyInUseError(email)
  }
  else {
    const user = new User(email);
    userRepository.add(user);
  }
}

这有效并消除了持久层中的任何溢出。但我必须在每个我想要添加用户的地方都这样做。你推荐的模式是什么?你有其他鼓励的方法吗?你会在哪里做这些检查来抛出错误

解决方法

完全依赖数据库功能来执行业务规则是一种不好的做法。

也就是说,鉴于在某些业务验证检查之后引发域异常这一事实,您不应从代表您的数据库(存储库)的类内部引发域异常。

域异常,顾名思义,应该在域(或应用程序)层内部使用。

因此,您的重复电子邮件验证应位于用例内部,然后是存储库操作(添加用户)。对于代码重复,解决方案很简单:使用包含此两阶段逻辑(验证然后操作)的方法创建域服务,并在您想要的任何地方使用此服务。

干净架构的一个关键原则是形成一个稳定的域层,同时让基础设施细节可以交换。但是,当您将业务规则放入存储库(基础架构)时,请考虑如果您决定创建替代存储库会发生什么:您必须记住将业务规则复制到新存储库中。

,

存储库通常在用例层中声明,因为它们是用例需要什么的定义。因此,这些接口应该是面向领域的。由于它们必须在外层实现,这意味着如果定义了域异常,则外层必须引发域异常。

但是这种实现是否可取?我们现在依赖于使用正确的唯一键模式正确设置的数据库来执行域规则(没有两个用户可以使用同一个电子邮件注册)

从用例的角度来看,它不依赖于数据库功能。您所描述的是存储库接口的一种实现。我的意思是如果实现是关系数据库的网关,您可能希望使用 db 约束来满足存储库接口的定义。但是您可能会使用另一个数据库甚至内存数据库。

要点是存储库接口以面向领域的方式描述了它想要什么,而不是它是如何完成的。一般来说,接口的本质是描述客户想要什么而不是如何。因此域约束仍然在接口处定义,例如

public interface UserRepository {

    /**
     *
     * throws an UserAlreadyExistsException if a user with the given email already exists.
     * returns the User created with the given arguments or throws an UserAlreadyExistsException. 
     *         Never returns null.
     */
    public User createUser(String email,....) throws UserAlreadyExistsException;

}

请记住,接口不仅仅是一个方法签名。它具有通常以非正式方式描述的前置和后置条件。

替代方案

例如,在 Java 中,如果您希望实现遵循您定义的路径,您也可以使用抽象类。由于我不知道您使用的是哪种语言,我将给您这个 Java 示例。

public abstract class UserRepository {
   
     public User createUser(String email,...) throws UserAlreadyExistsException {
        User user = findByEmail(email);

        if(user) {
            throw new UserAlreadyExistsException(email)
        } else {
            User user = new User(email);
            add(user);
        }
     }

     protected abstract findByEmail(String email);
     protected abstract add(User user);
}

但是当您使用抽象类时,您已经定义了实现的一部分。实现并不像接口示例中那样免费。并且您的实现必须扩展抽象类。这可能是一个问题,例如在 Java 中,因为 Java 不允许多重继承。因此,这取决于您使用的语言。

结论

我将使用第一个示例,只需定义一个抛出域异常的接口,并让实现选择如何完成。

当然这意味着我通常必须使用集成测试来测试实现,并且不能使用非常快的单元测试。但是使用单元测试仍然可以非常快速地测试用例。

我认为简洁的架构可以帮助您将大部分代码测试为纯粹且快速的单元测试。我们应该尝试以尽可能多的简单单元测试的方式来构建我们的代码。如果我们需要集成测试,我们应该尽可能快地完成它们,我们应该尽可能少地使用它们。