我正在构建一个尝试遵守干净架构的应用程序。我理解存储库旨在抽象持久层并以领域语言返回实体。但是,这是否意味着如果出现问题,它也应该检查并抛出域错误。让我们考虑一下我想通过用户存储库添加用户的情况。我可以做以下事情:
// 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);
}
}
这有效并消除了持久层中的任何溢出。但我必须在每个想要添加用户的地方都这样做。您会选择哪种推荐模式?您还有其他值得鼓励的方法吗?您会在哪里进行这些检查以引发错误。
完全依赖数据库功能来执行业务规则是一种不好的做法。
也就是说,考虑到在某些业务验证检查之后引发域异常,您不应该从代表数据库(存储库)的类内部引发域异常。
域异常,顾名思义,应该在域(或应用程序)层内部使用。
因此,您的重复电子邮件验证应位于用例内,然后是存储库操作(添加用户)。至于代码重复,解决方案很简单:使用包含此两阶段逻辑(验证然后操作)的方法创建一个域服务,并在您想要的任何地方使用此服务。
干净架构的一个关键原则是形成稳定的领域层,同时让基础设施细节可交换。但是,当您将业务规则放入存储库(基础设施)中时,请考虑如果您决定创建替代存储库会发生什么:您必须记住将业务规则复制到新存储库中。
存储库通常在用例层中声明,因为它们是用例所需内容的定义。因此,这些接口应该是面向领域的。由于它们必须在外层中实现,这意味着如果定义了域异常,则外层必须引发域异常。
但是这种实施是否可取?我们现在依靠使用正确的唯一密钥模式正确设置的数据库来强制执行域规则(没有两个用户可以使用同一电子邮件注册)
从用例的角度来看,接口如何实现并不重要。您可以实现数据库、文件或内存存储库,具体取决于存储库接口定义的实现方式。如果您实现关系数据库存储库,则可以使用数据库约束来满足存储库的接口定义。但您仍然必须将凸起的
ConstraintViolationException
映射到域异常。
要点是存储库接口是以面向领域的方式描述用例想要什么,而不是如何完成。任何界面的本质都是描述客户想要什么而不是如何想要。接口是为客户而不是为实现者设计的。
域约束在接口处定义,例如
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不允许多重继承。因此,这取决于您使用的语言。
结论
我将使用第一个示例,只需定义一个抛出域异常的接口,并让实现选择如何完成。
当然,这意味着我通常必须使用较慢的集成测试来测试实现,并且我不能使用快速的单元测试。但用例仍然可以通过单元测试轻松测试。
不。因为“用例”或“服务”应该在调用存储库之前捕获异常。