zoukankan      html  css  js  c++  java
  • 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    系列文章

    围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!
    ABP Framework 研习社(QQ群:726299208)
    ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!

    领域服务

    领域服务实现领域逻辑,它:

    • 依赖于服务仓储
    • 需要多个聚合,以实现单个聚合无法处理的逻辑。

    领域服务与领域对象一起使用,其方法可以获取和返回实体值对象、原始类型等。然而,它并不获取/返回DTOs,DTOs属于应用层。

    示例:将问题分配给用户

    回想一下,我们之前是如何实现将问题分配给用户的

    public class Issue:AggregateRoot<Guid>
    {
      //..
      //问题关联的用户ID
      public 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;}
      }
    }
    

    学习帮助

    围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

    ABP Framework 研习社(QQ群:726299208)
    专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!
    image

    记录技术修行中的反思与感悟,以码传心,以软制道,知行合一!
  • 相关阅读:
    安卓手机抓包
    探讨 yum 与 rpm 的安装包数量
    CentOS6.5 yum安装桌面环境
    CentOS6.5使用本地光盘做yum源 (参考:http://www.jb51.net/os/RedHat/43343.html)
    tar、zip 、unzip 打包与压缩 (参考:http://pengyl.blog.51cto.com/5591604/1191197)
    CentOS6.5使用本地光盘做yum源 (参考:http://www.jb51.net/os/RedHat/43343.html)
    mount 挂载光盘
    Shell编程
    Vim实现批量注释的方法
    Linux基本命令
  • 原文地址:https://www.cnblogs.com/YGYH/p/14934791.html
Copyright © 2011-2022 走看看