围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注! ABP Framework 研习社(QQ群:726299208) ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!
领域服务
领域服务实现领域逻辑,它:
•依赖于服务和仓储。•需要多个聚合,以实现单个聚合无法处理的逻辑。
领域服务与领域对象一起使用,其方法可以获取和返回实体、值对象、原始类型等。然而,它并不获取/返回DTOs,DTOs属于应用层。
示例:将问题分配给用户
回想一下,我们之前是如何实现将问题分配给用户的
public class Issue:AggregateRoot<Guid>
{//..//问题关联的用户IDpublic Guid? AssignedUserId{get;private set;}//分配方法public async Task AssignToAsync(AppUser user,IUserIssueService userIssueService){var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id);if(openIssueCount >=3 ){throw new BusinessException("IssueTracking:CanNotOpenLockedIssue");}AssignedUserId=user.Id;}public void CleanAssignment(){AssignedUserId=null;}
}
现在,我们将逻辑迁移到领域服务中。首先,修改 Issue 类:
public class Issue:AggregateRoot<Guid>
{//...public Guid? AssignedUserId{get;internal set;}
}
•在聚合中移除 AssignToAsync
方法(因为需要在对应的领域服务中实现该方法。)•将 AssignedUserId
属性设置器从私有改为内部internal
,以允许从领域服务中设置它。
接下来,创建一个领域服务 IssueManager
定义方法 AssignToAsync
将指定 Issue
分配给指定用户。
public class IssueManager:DomainService
{private readonly IRepository<Issue,Guid> _issueRepository;public IssueManager(IRepository<Issue,Guid> issueRepository){_issueRepository=issueRepository;}public async Task AssignToAsync(Issue issue,AppUser user){//获取关联用户处于打开状态问题的数量var openIssueCount=await _issueRepository.CountAsync(i=>i.AssingedUserId==user.Id && !i.IsClosed);//超过3个,则抛出异常if(openIssueCount>=3){throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");}issue.AssignedUserId=user.Id;}
}
IssueManager
在构造函数中注入需要的仓储,用于查询分配给用户处于打开状态的Issue。
建议使用
Manager
后缀命名来命名领域服务。
这种设计的唯一问题是:Issue.AssignedUserId
现在是 public
,可以在任何外部类中设置。然而,它不应该是公共的,访问范围应该是程序集内部internal
,只有在同一个程序集(IssueTracking.Domain
)项目中才可以调用。
这个例子的解决方案就是如此,我们认为这很合理:
•领域层开发者在使用 IssueManager 时,已经熟知领域规则。•应用层开发者强制使用 IssueManager,因此无法直接修改实体。
以上我们展示了将问题分配给用户的两种实现方式,两种方式权衡之下,我们更加推荐当业务逻辑需要与外部服务协同工作时,创建领域服务。
如果没有一个充分的理由,我们认为没有必要去为领域服务创建接口,比如:为
IssueManager
创建IIssueManger
接口。
应用服务
应用服务是无状态服务,实现应用程序用例。一个应用服务通常使用领域对象实现用例,获取或返回数据传输对象DTOs,被展示层调用。
应用服务通用原则:
•实现特定用例的应用逻辑,不能在应用服务中实现领域逻辑(需要理清应用逻辑和领域逻辑二者的区别)。•应用服务方法不能返回实体,因为这样会打破领域层的封装性,始终只返回DTO。
示例:分配问题给用户
using System;
using System.Threading.Tasks;
using IssueTracking.Users;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;namespace IssueTracking.Issues
{public class IssueAppService :ApplicationService.IIssueAppService{private readonly IssueManager _issueManager;private readonly IRepository<Issue,Guid> _issueRepository;private readonly IRepository<AppUser,Guid> _userRepository;public IssueAppService(IssueManager issueManager,IRepository<Issue,Guid> issueRepository,IRepository<AppUser,Guid> userRepository){_issueManager=issueManager;_issueRepository=issueRepository;_userRepository=userRepository;}[Authorize]public async Task AssignAsync(IssueAssignDto input){var issue=await _issueRepository.GetAsync(input.IssueId);var user=await _userRepository.GetAsync(inpu.UserId);await _issueManager.AssignToAsync(issue,user);await _issueRepository.UpdateAsync(issue);//没有对issue做任何修改,为什么要更新?在IssueManager中进行了状态修改。}}
}
一个应用服务方法通常有三个步骤:
•从数据库获取关联的领域对象•使用领域对象(领域服务、实体等)执行业务逻辑•在数据库中更新实体(如果已修改)
当时使用EF Core时,最后的 Update 更新操作并不是必须的,应为有 状态变更跟踪。但是建议显式调用,适配其他数据库提供程序。
示例中 IssueAssignDto
是一个简单的 DTO 类:
using System;
namespace IssueTracking.Issues
{public class IssueAssignDto{public Guid IssueId{get;set;}public Guid UserId{get;set;}}
}
学习帮助
围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实现、综合案例实现系列文章,敬请关注!
ABP Framework 研习社(QQ群:726299208) 专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!