ABP入门系列(18)—— 使用领域服务
1.引言
自上次更新有一个多月了,发现越往下写,越不知如何去写。特别是当遇到DDD中一些概念术语的时候,尤其迷惑。如果只是简单的去介绍如何去使用ABP,我只需参照官方文档,实现到任务清单Demo中去就可以了,不劳神不费力。但是,这样就等于一知半解。
知之为知之,不知为不知,是知也。知其然知其所以然,方能举一反三嘛。
为了揭开迷惑,最近开始研读《实现领域驱动设计》去学习DDD中的思想,并开了一个DDD专题去记录我学习的成果。欢迎大家关注,共同学习进步并不吝赐教!
后续的文章我会继续保持之前的书写风格,并适当穿插一些对DDD中的概念的理解,来加深对ABP框架的学习。
2.用例分析
用户可以无限创建任务但仅能分配给自己;管理员具有分配任务给他人的权限,任务分配成功后要通知接收人。
我们分析下这个业务用例,其实主要涉及到一个业务操作——任务分配。按照我们传统的思路,在做任务分配这个操作时,就是对任务进行编辑,没有什么特别的地方,通过代码调用应用层服务更新Task实体即可。
//TaskAppService.cs
public void UpdateTask(UpdateTaskInput input)
{
    //We can use Logger, it's defined in ApplicationService base class.
    Logger.Info("Updating a task for input: " + input);
    //获取是否有权限
    bool canAssignTaskToOther = PermissionChecker.IsGranted(PermissionNames.Pages_Tasks_AssignPerson);
    //如果任务已经分配且未分配给自己,且不具有分配任务权限,则抛出异常
    if (input.AssignedPersonId.HasValue && input.AssignedPersonId.Value != AbpSession.GetUserId() )
    {
        if (!canAssignTaskToOther)
            throw new AbpAuthorizationException("没有分配任务给他人的权限!");
        else
        {
            var updateTask = Mapper.Map<Task>(input);
            _taskRepository.Update(updateTask);
            //发送通知
            var message = "You hava been assigned one task into your todo list.";
            _smtpEmailSender.Send("ysjshengjie@qq.com", updateTask.AssignedPerson.EmailAddress, "New Todo item", message);
            _notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null,
                NotificationSeverity.Info, new[] { updateTask.AssignedPerson.ToUserIdentifier() });
        }
    }
}
以上代码也能满足以上需求,但是这已经违背了ABP分层架构的思想,其实也就是违背了DDD的思想。
应用层不包含业务逻辑,而我们的UpdateTask方法明显承载了太多的业务,既要检查权限又要发送通知。
那可如何是好?自然是在领域服务去处理这些业务逻辑了。
这里就引入了DDD中的两个概念,应用服务和领域服务,我们有必要先来介绍一下,之后再来用领域服务来改造。
3.应用服务VS领域服务
应用服务对应的是应用层,领域服务对应的领域层。它们之间主要的区别在于是否处理业务逻辑。那这个限制从何而来呢?DDD的分层架构思想。

- 用户接口层(Presentation):提供一个用户界面,实现用户交互操作。
- 应用层(Application):进行展现层与领域层之间的协调,协调业务对象来执行特定的应用程序的任务。它不包含业务逻辑。
- 领域层(Domain):包括业务对象和业务规则,这是应用程序的核心层。
- 基础设施层(Infrastructure):提供通用技术来支持更高的层。例如基础设施层的仓储(Repository)可通过ORM来实现数据库交互。
应用服务作为领域服务的消费方,领域服务是无状态的(领域对象具有状态和行为,而领域服务是用来处理业务逻辑的,它本身是一个行为,所以是无状态的)。领域服务是用来协调领域对象完成某个操作,状态由领域对象保存。
上面也说了,领域对象是具有状态和行为的。那就是说我们也可以在实体或值对象来处理业务逻辑。那我们该如何取舍呢?
一般来说,在下面的几种情况下,我们可以使用领域服务:
- 执行某个具体的业务操作。
- 领域对象的转换。
- 以多个领域对象为输入,返回一个值对象。
4. 使用领域服务
经过上面的分析,很显然我们的用例,使用领域服务来实现更合适。
ABP中定义了IDomainService接口,按约定所有的领域服务都要实现它,实现之后,领域服务被自动暂时的注册到依赖注入系统。
同样,领域服务也可以从DomainService类继承,因此它可以使用继承得来的日志、本地化、等属性。
这里,我们定义一个ITaskManager(Abp中约定俗成的领域服务命名规则,以Manager结尾,当然你也可以自行命名)来定义我们的领域服务,然后实现它。
namespace LearningMpaAbp.Tasks
{
    public interface ITaskManager : IDomainService
    {
        void AssignTaskToPerson(Task task, User user);
    }
}
实现的领域服务负责主要的业务逻辑,其中发送通知的业务我定义了一个领域事件去实现。关于领域事件,我们下节再聊。
namespace LearningMpaAbp.Tasks
{
    public class TaskManager : DomainService, ITaskManager
    {
        private readonly IPermissionChecker _permissionChecker;
        private readonly IAbpSession _abpSession;
        private readonly IEventBus _eventBus;
        public TaskManager(IPermissionChecker permissionChecker, IAbpSession abpSession, IEventBus eventBus)
        {
            _permissionChecker = permissionChecker;
            _abpSession = abpSession;
            _eventBus = eventBus;
        }
        public void AssignTaskToPerson(Task task, User user)
        {
            //已经分配,就不再分配
            if (task.AssignedPersonId.HasValue && task.AssignedPersonId.Value == user.Id)
            {
                return;
            }
            if (task.State != TaskState.Open)
            {
                throw new ApplicationException("处于非活动状态的任务不能分配!");
            }
            //获取是否有【分配任务给他人】的权限
            bool canAssignTaskToOther = _permissionChecker.IsGranted(PermissionNames.Pages_Tasks_AssignPerson);
            if (user.Id != _abpSession.GetUserId() && !canAssignTaskToOther)
            {
                throw new AbpAuthorizationException("没有分配任务给他人的权限!");
            }
            task.AssignedPersonId = user.Id;
            //使用领域事件触发发送通知操作
            _eventBus.Trigger(new TaskAssignedEventData(task, user));
        }
    }
}
定义完领域服务,我们直接在应用服务层调用即可。
public void UpdateTask(UpdateTaskInput input)
{
    //We can use Logger, it's defined in ApplicationService base class.
    Logger.Info("Updating a task for input: " + input);
    //获取是否有权限
    //bool canAssignTaskToOther = PermissionChecker.IsGranted(PermissionNames.Pages_Tasks_AssignPerson);
    //如果任务已经分配且未分配给自己,且不具有分配任务权限,则抛出异常
    if (input.AssignedPersonId.HasValue && input.AssignedPersonId.Value != AbpSession.GetUserId())
    {
        var updateTask = Mapper.Map<Task>(input);
        var user = _userRepository.Get(input.AssignedPersonId.Value);
        //先执行分配任务
        _taskManager.AssignTaskToPerson(updateTask, user);
        //再更新其他字段
        _taskRepository.Update(updateTask);
        ////发送通知
        //var message = "You hava been assigned one task into your todo list.";
        //_smtpEmailSender.Send("ysjshengjie@qq.com", updateTask.AssignedPerson.EmailAddress, "New Todo item", message);
        //_notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null,
        //    NotificationSeverity.Info, new[] { updateTask.AssignedPerson.ToUserIdentifier() });
    }
}
更新后UpdateTask方法已经注释掉了权限检查以及发布通知的业务逻辑,整个方法也讲更清晰。
5.总结
这一节没有太难的知识点,我们只需谨记,领域服务和应用服务的区别在于只有领域服务才处理业务逻辑。应用服务作为领域服务的消费方,是很薄的一层。
当然,我们也要记住,过度使用领域服务会导致贫血领域模型(即所有的业务逻辑都位于领域服务中,而不是实体和值对象中)。
ABP入门系列(18)—— 使用领域服务的更多相关文章
- ABP入门系列(3)——领域层创建实体
		这一节我们主要和领域层打交道.首先我们要对ABP的体系结构以及从模板创建的解决方案进行一一对应.网上有代码生成器去简化我们这一步的任务,但是不建议初学者去使用. 一.首先来看看ABP体系结构 领域层就 ... 
- ABP入门系列(19)——使用领域事件
		ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 1.引言 最近刚学习了下DDD中领域事件的理论知识,总的来说领域事件主要有两个作用,一是解耦,二是 ... 
- ABP入门系列(2)——领域层创建实体
		ABP入门系列目录--学习Abp框架之实操演练 这一节我们主要和领域层打交道.首先我们要对ABP的体系结构以及从模板创建的解决方案进行一一对应.网上有代码生成器去简化我们这一步的任务,但是不建议初学者 ... 
- ABP入门系列(4)——领域层定义仓储并实现
		一.先来介绍下仓储 仓储(Repository): 仓储用来操作数据库进行数据存取.仓储接口在领域层定义,而仓储的实现类应该写在基础设施层. 在ABP中,仓储类要实现IRepository接口,接口定 ... 
- ABP入门系列(3)——领域层定义仓储并实现
		ABP入门系列目录--学习Abp框架之实操演练 一.先来介绍下仓储 仓储(Repository): 仓储用来操作数据库进行数据存取.仓储接口在领域层定义,而仓储的实现类应该写在基础设施层. 在ABP中 ... 
- ABP入门系列目录——学习Abp框架之实操演练
		ABP是"ASP.NET Boilerplate Project (ASP.NET样板项目)"的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WE ... 
- ABP入门系列(13)——Redis缓存用起来
		ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 1. 引言 创建任务时我们需要指定分配给谁,Demo中我们使用一个下拉列表用来显示当前系统的所有用 ... 
- ABP入门系列(1)——学习Abp框架之实操演练
		作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ... 
- ABP入门系列(5)——创建应用服务
		一.解释下应用服务层 应用服务用于将领域(业务)逻辑暴露给展现层.展现层通过传入DTO(数据传输对象)参数来调用应用服务,而应用服务通过领域对象来执行相应的业务逻辑并且将DTO返回给展现层.因此,展现 ... 
随机推荐
- linux sed命令就是这么简单
			概述 sed命令是一个面向字符流的非交互式编辑器,也就是说sed不允许用户与它进行交互操作.sed是按行来处理文本内容的.在shell中,使用sed来批量修改文本内容是非常方便的. sed命令的选项 ... 
- 【模板】二分图最大权完美匹配KM算法
			hdu2255模板题 KM是什么意思,详见百度百科. 总之知道它可以求二分图最大权完美匹配就可以了,时间复杂度为O(n^3). 给张图. 二分图有了边权,求最大匹配下的最大权值. 所以该怎么做呢?对啊 ... 
- 【HDOJ 1085】数学问题,母函数
			Holding Bin-Laden Captive! Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Ja ... 
- PHP的laravel框架后台实现数据导出excel的功能
			要想在PHP后台实现excel导入导出功能,一种简单有效的方法就是使用phpexcel插件. 要使用phpexcel插件,首先需要下载composer,这个工具是专门用来管理项目中库之间的依赖关系的. ... 
- pixi.js
			添加基本文件(库文件) 渲染库 pixi.js pixi.lib.js是pixi.js的子集,依赖class.js,cat.js,event_emiter.js文件 pixi.scroller.js ... 
- Java--向数据库添加txt文件中的批量数据
			大家可能会遇到这样的问题,在做一个项目时需要操作数据库,需要有大量的数据需要导入到数据库中,这部分数据存到了txt文档中(可以把Word文档中的数据弄到txt文档),总不能每一条数据都复制黏贴到数据库 ... 
- React的学习(上)
			摘要 众所周知,前端三大框架Angular.React.Vue,所以为了跟上时代的步伐,最近开始着手学习React,这时候就发现个大问题,框架一直在更新,提倡的编写语法也在更新,网上有许多教程都是一两 ... 
- uiautomator+cucumber实现自动化测试
			前提 由于公司业务要求,所以自动化测试要达到以下几点: 跨应用的测试 测试用例可读性强 测试报告可读性强 对失败的用例有截图保存并在报告中体现 基于以上几点,在对自动化测试框架选型的时候就选择了uia ... 
- 某公司HP-EVA4400数据丢失的恢复方法和恢复全过程
			一.故障描述1.设备清单一台HP-EVA4400控制器(型号:AG638-53011)三台HP-EVA4400扩展柜(型号为AG638-63001),和28块HP-FC磁盘(型号为300G FC硬盘) ... 
- css3常用方法以及css3选择器
			最重要的 CSS3 模块包括: 选择器 框模型 背景和边框 文本效果 2D/3D 转换 动画 多列布局 用户界面 CSS3 边框 CSS3 边框 通过 CSS3,您能够创建圆角边框,向矩形添加 ... 
