在设计聚合和实体时,最好只向消费者提供聚合的公共属性和方法。但碰巧为了改变某些属性,需要执行复杂的业务逻辑案例。
例如,我们有一个用户聚合,它有一个电子邮件属性。要更改电子邮件,您需要确保没有其他注册用户使用同一电子邮件。这意味着您需要运行带有数据库搜索的复杂脚本。将存储库(或存储库接口)嵌入到聚合本身的方法中是一种不好的做法,我们会得到进程外依赖项。因此,您需要将此代码放在 DomainService 或 AppService 中(取决于方法)。这意味着该属性将是内部或公共的,并且可以在程序集内部或外部进行更改。这意味着任何人都可以错误地更改它,绕过服务中描述的业务规则。
public class User : IAggregateRoot
{
public int Id { get; }
public string Email { get; set; }
private User(int id, string email)
{
Id = id;
Email = email;
}
}
public class UserService
{
private IUserRepository repository;
public UserService(IUserRepository repository)
{
this.repository = repository;
}
public async Task ChangeEmail(int userId, string email)
{
var exist = await repository.GetByEmailAsync(email);
if (exist != null)
throw new DomainException("Email is already in use");
var user = await repository.GetByIdAsync(userId);
user.Email = email;
await repository.SaveAsync(user);
}
}
如果该财产是私人的,可以进行修改,那就太好了。从其他类的访问只能通过 ChangeEmail 方法。
public string Email { get; private set; }
如果 C# 中存在友好类,则可以解决此问题,然后可以向服务授予对聚合或实体的私有属性和方法的访问权限。
如果程序集可以相互引用(如图所示),这个问题也可以得到解决,但 C# 中禁止循环依赖。 (图中的每个方块都是一个单独的项目)
您可以尝试使用可以访问私有属性和方法的嵌套类,但是对于聚合和与聚合关联的实体来说,嵌套类不能相同。例如,Order 是聚合,OrderItem 是实体。它不能同时嵌套在两个类中。
你可以这样做:
public class User : IAggregateRoot
{
public int Id { get; }
public string Email { get; private set; }
private User(int id, string email)
{
Id = id;
Email = email;
}
public void ChangeEmail(string email)
{
Email = email;
//... maybe trigger a domain event here
}
}
public class UserService
{
private IUserRepository repository;
public UserService(IUserRepository repository)
{
this.repository = repository;
}
public async Task ChangeEmail(int userId, string email)
{
var exist = repository.GetByEmailAsync(email);
if (exist != null)
throw new DomainException("Email is already in use");
var user = await repository.GetByIdAsync(userId);
user.ChangeEmail(email);
await repository.SaveAsync(user);
}
}