DDD中全局规则validation的位置
我是DDD的新手,我试图将其应用于现实生活中。 没有关于这种validation逻辑的问题,如空检查,空string检查等 – 直接进入实体构造函数/属性。 但是,在哪里可以validation一些全球规则,如“唯一用户名”?
所以,我们有实体用户
public class User : IAggregateRoot { private string _name; public string Name { get { return _name; } set { _name = value; } } // other data and behavior }
和用户的存储库
public interface IUserRepository : IRepository<User> { User FindByName(string name); }
选项是:
- 将资源库注入实体
- 将库注入工厂
- 创build域服务的操作
- ???
而每个选项更详细:
1。将资源库注册到实体
我可以在实体构造函数/属性中查询存储库。 但我认为在实体中保持对存储库的引用是一种难闻的气味。
public User(IUserRepository repository) { _repository = repository; } public string Name { get { return _name; } set { if (_repository.FindByName(value) != null) throw new UserAlreadyExistsException(); _name = value; } }
更新:我们可以使用DI通过Specification对象隐藏User和IUserRepository之间的依赖关系。
2.将库注入工厂
我可以把这个validation逻辑放在UserFactory中。 但是如果我们想改变已经存在的用户的名字呢?
3.在域服务上创build操作
我可以创build用于创build和编辑用户的域服务。 但是,有人可以直接编辑用户的名字,而无需调用该服务…
public class AdministrationService { private IUserRepository _userRepository; public AdministrationService(IUserRepository userRepository) { _userRepository = userRepository; } public void RenameUser(string oldName, string newName) { if (_userRepository.FindByName(newName) != null) throw new UserAlreadyExistException(); User user = _userRepository.FindByName(oldName); user.Name = newName; _userRepository.Save(user); } }
4.
你在哪里把实体的全局validation逻辑?
谢谢!
大多数情况下,最好将这些规则放置在Specification
对象中。 您可以将这些Specification
放在您的域包中,这样任何使用您的域包的人都可以访问它们。 使用规范,您可以将您的业务规则与您的实体捆绑在一起,而不会创build对服务和存储库有不良依赖的难以阅读的实体。 如果需要,可以将服务或存储库的dependency injection到规范中。
根据上下文,可以使用规范对象构build不同的validation器。
实体的主要关注点应该是跟踪业务状态 – 这是足够的责任,他们不应该关心validation。
例
public class User { public string Id { get; set; } public string Name { get; set; } }
两个规格:
public class IdNotEmptySpecification : ISpecification<User> { public bool IsSatisfiedBy(User subject) { return !string.IsNullOrEmpty(subject.Id); } } public class NameNotTakenSpecification : ISpecification<User> { // omitted code to set service; better use DI private Service.IUserNameService UserNameService { get; set; } public bool IsSatisfiedBy(User subject) { return UserNameService.NameIsAvailable(subject.Name); } }
一个validation器:
public class UserPersistenceValidator : IValidator<User> { private readonly IList<ISpecification<User>> Rules = new List<ISpecification<User>> { new IdNotEmptySpecification(), new NameNotEmptySpecification(), new NameNotTakenSpecification() // and more ... better use DI to fill this list }; public bool IsValid(User entity) { return BrokenRules(entity).Count() > 0; } public IEnumerable<string> BrokenRules(User entity) { return Rules.Where(rule => !rule.IsSatisfiedBy(entity)) .Select(rule => GetMessageForBrokenRule(rule)); } // ... }
为了完整性,接口:
public interface IValidator<T> { bool IsValid(T entity); IEnumerable<string> BrokenRules(T entity); } public interface ISpecification<T> { bool IsSatisfiedBy(T subject); }
笔记
我认为维杰·帕特尔早先的答案是朝着正确的方向发展,但是我觉得这有点不合适。 他build议用户实体依赖于规范,我相信这应该是相反的。 通过这种方式,您可以让规范依赖于服务,存储库和上下文,而不会使您的实体通过规范依赖关系依赖于它们。
参考
一个相关的问题,以一个很好的答案为例: 在领域驱动devise中的validation 。
Eric Evans在第9章第145页描述了validation,select和对象构造的规范模式的使用。
这篇关于 .Net中的应用程序的规范模式的文章可能是你感兴趣的。
如果是用户input,我不会build议不允许更改实体的属性。 例如,如果validation没有通过,您仍然可以使用该实例将其显示在带有validation结果的用户界面中,从而允许用户更正错误。
吉米·尼尔森(Jimmy Nilsson)在其“应用领域驱动的devise和模式”中build议validation特定的操作,而不仅仅是为了坚持。 虽然一个实体可以被成功持久化,但是当一个实体即将改变它的状态,例如“有序”状态改变为“已购买”时,真正的确认就会发生。
在创build时,实例必须是有效的,以保存,这涉及到检查唯一性。 它不同于有效的订购,不仅要检查唯一性,还要考虑客户的可信度,以及商店的可用性。
所以,validation逻辑不应该在属性赋值上被调用,它应该在聚合级别的操作上被调用,不pipe它们是否是持久的。
编辑:从其他答案来看,这样的“域名服务”的正确名称是规范 。 我已经更新了我的答案以反映这一点,包括更详细的代码示例。
我会select3; 创build封装了执行validation的实际逻辑的域服务规范。 例如,规范最初会调用一个存储库,但是您可以稍后用Web服务调用来replace它。 拥有抽象规范背后的所有逻辑将使整体devise更加灵活。
为了防止某人在不validation的情况下编辑该名称,请将该规范作为编辑该名称的必要方面。 你可以通过改变实体的API来达到这个目的:
public class User { public string Name { get; private set; } public void SetName(string name, ISpecification<User, string> specification) { // Insert basic null validation here. if (!specification.IsSatisfiedBy(this, name)) { // Throw some validation exception. } this.Name = name; } } public interface ISpecification<TType, TValue> { bool IsSatisfiedBy(TType obj, TValue value); } public class UniqueUserNameSpecification : ISpecification<User, string> { private IUserRepository repository; public UniqueUserNameSpecification(IUserRepository repository) { this.repository = repository; } public bool IsSatisfiedBy(User obj, string value) { if (value == obj.Name) { return true; } // Use this.repository for further validation of the name. } }
你的调用代码看起来像这样:
var userRepository = IoC.Resolve<IUserRepository>(); var specification = new UniqueUserNameSpecification(userRepository); user.SetName("John", specification);
当然,你可以在你的unit testing中嘲笑ISpecification
,以便于testing。
我会使用规范来封装规则。 您可以在UserName属性更新(或从其他可能需要的地方)时调用:
public class UniqueUserNameSpecification : ISpecification { public bool IsSatisifiedBy(User user) { // Check if the username is unique here } } public class User { string _Name; UniqueUserNameSpecification _UniqueUserNameSpecification; // You decide how this is injected public string Name { get { return _Name; } set { if (_UniqueUserNameSpecification.IsSatisifiedBy(this)) { _Name = value; } else { // Execute your custom warning here } } } }
如果另一个开发人员试图直接修改User.Name
,则User.Name
,因为该规则将始终执行。
在这里了解更多
我不是DDD方面的专家,但是我问了自己相同的问题,这就是我想到的:validation逻辑通常应该进入构造函数/工厂和设置器。 这样你保证你总是有有效的域对象。 但是如果validation涉及影响性能的数据库查询,则有效的实现需要不同的devise。
(1)注入实体注入实体可能是技术上的困难,并且由于数据库逻辑的碎片化,也使得pipe理应用程序性能非常困难。 看似简单的操作现在可以产生意想不到的性能影响。 它也使得不可能优化你的域对象在同类实体的组上的操作,你不再可以写一个单一的组查询,而是每个实体总是有单独的查询。
(2)注入存储库:你不应该把任何业务逻辑放在存储库中。 保持知识库简单和专注。 它们应该像收集一样,只包含添加,删除和查找对象(有些甚至将查找方法剥离到其他对象)的逻辑。
(3)域服务这似乎是处理需要数据库查询的validation的最合理的地方。 一个好的实现将使构造函数/工厂和设置器涉及包私有,以便实体只能使用域服务创build/修改。
在我的CQRS框架中,每个Command Handler类都包含一个ValidateCommand方法,然后调用Domain中的相应业务/validation逻辑(主要实现为Entity方法或Entity静态方法)。
所以来电者会这样做:
if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK) { // Now we can assume there will be no business reason to reject // the command cmdService.ExecuteCommand(myCommand); // Async }
每个专门的命令处理程序都包含封装逻辑,例如:
public ValidationResult ValidateCommand(MakeCustomerGold command) { var result = new ValidationResult(); if (Customer.CanMakeGold(command.CustomerId)) { // "OK" logic here } else { // "Not OK" logic here } }
然后,命令处理程序的ExecuteCommand方法将再次调用ValidateCommand(),所以即使客户端没有打扰,域中也不会有任何事情发生。
创build域服务
或者我可以创build用于创build和编辑用户的域服务。 但是,有人可以直接编辑用户的名字,而无需调用该服务…
如果你正确地devise你的实体,这不应该是一个问题。
创build一个方法,例如,称为IsUserNameValid()并使其可以从任何地方访问。 我会自己把它放在用户服务。 这样做将不会限制你未来的变化。 它将validation代码保存在一个地方(实现),如果validation发生变化,其他代码将不必更改。您可能会发现,您需要稍后从多个地方调用此代码,例如用于可视化指示的ui而不必求助于exception处理。 用于正确操作的服务层以及存储库(cache,db等)层确保存储的项目是有效的。
我喜欢选项3.最简单的实现可能看起来如此:
public interface IUser { string Name { get; } bool IsNew { get; } } public class User : IUser { public string Name { get; private set; } public bool IsNew { get; private set; } } public class UserService : IUserService { public void ValidateUser(IUser user) { var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed if (user.IsNew && repository.UserExists(user.Name)) throw new ValidationException("Username already exists"); } }