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