在 DDD 中将全局规则验证放在哪里

我是 DDD 的新手,我正试着把它应用到现实生活中。对于直接进入实体构造函数/属性的验证逻辑,如 null 检查、空字符串检查等,没有任何问题。但是像“唯一用户名”这样的全局规则的验证应该放在哪里呢?

所以,我们有实体用户

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

选择包括:

  1. 向实体注入存储库
  2. 向工厂注入仓库
  3. 创建域服务操作
  4. ???

每个选项都更加详细:

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 通过规范对象隐藏 User 和 IUserRepository 之间的依赖关系。

2. 向工厂注入仓库

我可以把这个验证逻辑放到 UserFactory 中,但是如果我们想要更改已经存在的用户的名称呢?

3. 创建域服务操作

我可以为创建和编辑用户创建域服务。但有人可以直接编辑用户的名称,而无需调用该服务..。

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个?

实体的全局验证逻辑放在哪里?

谢谢!

19187 次浏览

创建域服务

或者我可以创建域服务 创建和编辑用户。但是 可以直接编辑用户名 不打电话给服务中心。

如果您正确地设计了您的实体,这应该不是一个问题。

编辑: 从其他答案来看,这种“域服务”的正确名称是 规格。我已经更新了我的答案来反映这一点,包括一个更详细的代码示例。

我会选择选项3; 创建一个 域名服务规范,它封装执行验证的实际逻辑。例如,规范最初调用存储库,但是您可以在稍后的阶段用 Web 服务调用替换它。在抽象规范背后拥有所有这些逻辑将使整个设计更加灵活。

为了防止有人在没有验证名称的情况下编辑名称,请将规范作为编辑名称的一个必要方面。您可以通过将实体的 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);

当然,您可以在单元测试中模拟 ISpecification,以便更容易地进行测试。

我不建议禁止更改实体中的属性,如果它是用户输入的话。 例如,如果验证没有通过,您仍然可以使用实例在用户界面中显示验证结果,从而允许用户更正错误。

Jimmy Nilsson 在他的“应用领域驱动设计和模式”中建议对特定的操作进行验证,而不仅仅是为了持久化。虽然可以成功地持久化一个实体,但真正的验证发生在一个实体即将改变其状态时,例如,“已订购”状态改为“已购买”状态。

在创建时,实例必须是有效的-for-save,这涉及到检查惟一性。它不同于有效排序,在有效排序中,不仅必须检查唯一性,而且还必须检查客户机的可信性和存储区的可用性。

因此,不应该在属性分配上调用验证逻辑,而应该在聚合级别操作上调用验证逻辑,无论这些操作是持久的还是非持久的。

我不是 DDD 方面的专家,但是我也问过自己同样的问题,这就是我想到的: 验证逻辑通常应该进入构造函数/工厂和设置器。这样可以保证始终拥有有效的域对象。但是,如果验证涉及影响性能的数据库查询,则有效的实现需要不同的设计。

(1)注入实体: 由于数据库逻辑支离破碎,注入实体可能在技术上有困难,并且使得管理应用程序性能变得非常困难。看似简单的操作现在可以产生意想不到的性能影响。它还使得不可能优化您的域对象,以便对同类实体的组进行操作,您不再能够编写单个组查询,而是始终为每个实体提供单个查询。

(2)注入存储库: 不应该在存储库中放置任何业务逻辑。使存储库保持简单和集中。它们应该像集合一样工作,只包含用于添加、删除和查找对象的逻辑(有些甚至将 find 方法派生到其他对象中)。

(3)域服务 这似乎是处理需要数据库查询的验证的最合理的地方。一个好的实现将使包中涉及的构造函数/工厂和设置器私有化,这样实体只能通过域服务创建/修改。

我将使用 规格来封装规则。然后,您可以在 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,这并不重要,因为规则将始终执行。

你可在此找到更多资料

大多数情况下,最好将这类规则放在 Specification对象中。 您可以将这些 Specification放在您的域包中,这样任何使用您的域包的人都可以访问它们。通过使用规范,您可以将业务规则与实体捆绑在一起,而无需创建具有不希望的依赖于服务和存储库的难以阅读的实体。如果需要,您可以将对服务或存储库的依赖注入到规范中。

根据上下文,您可以使用规范对象构建不同的验证器。

实体的主要关注点应该是跟踪业务状态——这是一个足够大的责任,他们不应该关心验证。

例子

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

还有验证器:

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

笔记

我认为维杰 · 帕特尔早期的答案是正确的,但我觉得有点不对劲。他认为用户实体取决于规范,而我认为规范应该是反过来的。通过这种方式,您可以让规范通常依赖于服务、存储库和上下文,而无需通过规范依赖性使您的实体依赖于它们。

参考文献

一个相关的问题和一个很好的答案与例子: 领域驱动设计中的验证

EricEvans 描述了在 第9章,第145页中使用规范模式进行验证、选择和对象构造。

您可能会感兴趣的是使用.Net 编写应用程序的 关于规范模式的文章

在我的 CQRS 框架中,每个 Command Handler 类还包含一个 ValidateCommand 方法,然后该方法调用域中适当的业务/验证逻辑(主要实现为 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
}

例如,每个专门的 Command Handler 都包含包装器逻辑:

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
var result = new ValidationResult();
if (Customer.CanMakeGold(command.CustomerId))
{
// "OK" logic here
} else {
// "Not OK" logic here
}
}

然后,命令处理程序的 ExecuteCommand 方法将再次调用 ValidateCommand () ,因此即使客户机没有这样做,也不会在 Domain 中发生不应该发生的任何事情。

例如,创建一个名为 IsUserNameValid ()的方法,并使其可从任何地方访问。我会自己把它放到用户服务里。这样做不会在将来出现更改时限制您。它将验证代码保存在一个地方(实现) ,如果验证发生变化,其他依赖于验证的代码将不必更改。您可能会发现,以后需要从多个地方调用验证代码,比如用于可视化指示的 UI,而不必求助于异常处理。用于正确操作的服务层和用于确保存储项有效的存储库(缓存、数据库等)层。

我喜欢选项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");
}
}

简而言之,你有4种选择:

  • IsValid 方法: 将一个实体转换到一个状态(可能无效) ,并要求它验证自己。

  • 应用程序服务中的验证。

  • 尝试执行模式。

  • 执行/可执行模式。

阅读更多 给你