ABP官方文档翻译 3.4 领域服务
领域服务
介绍
领域服务(或者在DDD中单纯的服务)用来执行领域操作和业务规则。Eric Evans在他的DDD书中描述了一个好的服务有三个特征:
1. 与领域概念关联的操作,但不是实体或值对象的自然组成部分。
2. 接口的定义依照领域模型的其他元素。
3. 操作是无状态的。
不像应用服务那样获取或返回DTO,领域服务获取或返回领域对象(如实体或值对象)。
领域服务可以被应用服务或其他领域服务使用,但不能被展现层(应用层可以)直接使用。
IDomainService接口和DomainService类
ABP定义了IDomainService接口,并约定所有的领域服务都实现这个接口。当被实现时,领域服务自动注册到依赖注入系统,调用类型为临时的。
领域服务可以继承DomainService类。从而,可以使用一些继承属性来记录日志,本地化等等。当然,即使没有继承,如果需要的话也可以注入他们。
示例
假定我们有一个任务管理系统,当分配任务到一个人的时候需要执行业务规则。
创建接口
首先,我们定义这个服务的接口(不是必须,但最好这样作为最佳实践):
public interface ITaskManager : IDomainService
{
void AssignTaskToPerson(Task task, Person person);
}
如上所见,TaskManager服务使用领域对象工作:Task和Person。命名领域服务有些约定,可以为TaskManager、TaskService或TaskDomainService.....
服务实现
让我们来看看如何实现:
public class TaskManager : DomainService, ITaskManager
{
public const int MaxActiveTaskCountForAPerson = ; private readonly ITaskRepository _taskRepository; public TaskManager(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
} public void AssignTaskToPerson(Task task, Person person)
{
if (task.AssignedPersonId == person.Id)
{
return;
} if (task.State != TaskState.Active)
{
throw new ApplicationException("Can not assign a task to a person when task is not active!");
} if (HasPersonMaximumAssignedTask(person))
{
throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name));
} task.AssignedPersonId = person.Id;
} private bool HasPersonMaximumAssignedTask(Person person)
{
var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id);
return assignedTaskCount >= MaxActiveTaskCountForAPerson;
}
}
这里,我们有两条业务规则:
- 一个任务的状态为Active,才可以分配给一个新的人。
- 一个人最多有3个Active状态的任务。
你可能会想为什么第一次检查时抛出ApplicationException而第二次检查时抛出UserFriendlyException。这个和领域服务没有任何关系。在这里仅仅是个例子,抛出哪种类型异常完全由我们自己决定。我认为,用户接口必须检查任务状态,并且不允许我们分配任务到人,这是一个应用错误,应该对用户不可见。第二个对UI来讲是很难检查到的,我们可以给用户一个易读的错误信息。
使用应用服务
现在,我们看看如何从应用服务中使用TaskManager:
public class TaskAppService : ApplicationService, ITaskAppService
{
private readonly IRepository<Task, long> _taskRepository;
private readonly IRepository<Person> _personRepository;
private readonly ITaskManager _taskManager; public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository, ITaskManager taskManager)
{
_taskRepository = taskRepository;
_personRepository = personRepository;
_taskManager = taskManager;
} public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId); _taskManager.AssignTaskToPerson(task, person);
}
}
任务应用服务使用指定的DTO(input)和仓储获取相关的任务和人,并把他们传递给Task Manager(领域服务)。
一些探讨
基于上面的示例,你可能有些问题。
为什么只有应用服务?
你可能会说为什么应用服务不实现在领域服务的逻辑?
我们可以简单来说,这不是应用服务的任务。因为这不是一个用例而是业务操作。我们或许会使用同样的“分配一个任务到人”的领域逻辑在不同的用例下。比方说,我们可能有另一个场景,某种情况下更新了这个任务并且这个更新包含分配这个任务到另一个人。所以,我们可以在这里使用相同的领域逻辑。还有,我们可能有两个不同的UI(一个移动应用和一个web应用)共享相同的领域或者我们可能有有一个为远程客户端使用的Web Api,而远程客户端包含分配任务的操作。
如果你的领域比较简单,只有一个UI并且分配任务到人只在一个点完成,那么可以考虑跳过领域服务,直接在应用服务实现逻辑。这对DDD来讲不是最佳实践,但是ABP并不强制。
如何强制使用领域服务?
可以看到,应用服务可以很容易的使用领域服务:
public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
task.AssignedPersonId = input.PersonId;
}
写领域服务的开发者可能不知道有一个TaskManager,并且可以直接将给定的PersonId设置给任务的AssignedPersonId。所以,如何禁止它?在DDD领域有很多讨论,这有些有用的模式。我们不会很深入,但是会提供一个实现的简单方式。
我们可以按如下更改Task实体:
public class Task : Entity<long>
{
public virtual int? AssignedPersonId { get; protected set; } //...other members and codes of Task entity public void AssignToPerson(Person person, ITaskPolicy taskPolicy)
{
taskPolicy.CheckIfCanAssignTaskToPerson(this, person);
AssignedPersonId = person.Id;
}
}
我们更改了AssignedPersonId的setter为protected。所以,在Task实体类之外将不能更改它。添加一个AssignToPerson方法,接收一个person和task plicy。CheckIfCanAssignTaskToPerson方法检查是否为有效的分配,如果无效则抛出一个合适的异常(它的实现在这里不重要)。然后应用服务方法将变为如下所示:
public void AssignTaskToPerson(AssignTaskToPersonInput input)
{
var task = _taskRepository.Get(input.TaskId);
var person = _personRepository.Get(input.PersonId); task.AssignToPerson(person, _taskPolicy);
}
我们注入了ITaskPolicy作为_taskPolicy并把他传递给AssignToPersion方法。现在,没有第二种方式分配一个任务到人了。我们应该总是使用AssignToPerson方法,并且不能跳过业务规则。
ABP官方文档翻译 3.4 领域服务的更多相关文章
- ABP官方文档翻译 3.7 领域事件(事件总线)
领域事件(事件总线) 事件总线 注入IEventBus 获取默认实例 定义事件 预定义事件 处理异常 实体更改 触发事件 处理事件 处理基础事件 处理者异常 处理多个事件 注册处理者 自动 手动 取消 ...
- ABP官方文档翻译 3.6 工作单元
工作单元 介绍 ABP中的连接和事务管理 传统的工作单元方法 控制工作单元 UnitOfWork特性 IUnitOfWorkManager 工作单元详情 禁用工作单元 无事务工作单元 一个工作单元方法 ...
- ABP官方文档翻译 3.5 规约
规约 介绍 示例 创建规范类 使用仓储规约 组合规约 讨论 什么时候使用? 什么时候不使用? 介绍 规约模式是一种特别的软件设计模式,通过使用布尔逻辑将业务规则链接起来重新调配业务规则.(维基百科). ...
- ABP官方文档翻译 0.0 ABP官方文档翻译目录
一直想学习ABP,但囿于工作比较忙,没有合适的契机,当然最重要的还是自己懒.不知不觉从毕业到参加工作七年了,没留下点儿什么,总感觉很遗憾,所以今天终于卯足劲鼓起勇气开始写博客.有些事能做的很好,但要跟 ...
- ABP官方文档翻译 2.1 依赖注入
依赖注入 什么是依赖注入 传统方式的问题 解决方案 构造函数注入模式 属性注入模式 依赖注入框架 ABP依赖注入基础设施 注册依赖注入 传统注册 帮助接口 自定义/直接注册 使用IocManager ...
- ABP官方文档翻译 1.2 N层架构
N层架构 介绍 ABP架构 其他(通用) 领域层 应用层 基础设施层 网络和展现层 其他 总结 介绍 应用程序代码库的分层架构是被广泛认可的可以减少程序复杂度.提高代码复用率的技术.为了实现分层架构, ...
- ABP官方文档翻译 2.6 定时
定时 介绍 时钟 客户端 时区 客户端 Binders和Converters 介绍 一些应用只针对一个时区,而其他的一些已用则有许多不同的时区.为了满足这样的需求和集中的时间操作,Abp提供了时间操作 ...
- ABP官方文档翻译 4.1 应用服务
应用服务 IApplicationService接口 ApplicationService类 CrudService和AsyncCrudAppService类 简单的CRUD应用服务示例 自定义CRU ...
- ABP官方文档翻译 3.3 仓储
仓储 默认仓储 自定义仓储 自定义仓储接口 自定义仓储实现 基础仓储方法管理数据库连接 查询 获取单个实体 获取实体列表 关于IQueryable 自定义返回值 插入 更新 删除 其他 关于异步方法 ...
随机推荐
- [51nod1440]迈克打电话
有n只熊,从1到n进行编号. 第i只熊的电话号码是si.每只熊会给那些电话号码是他的子串的熊打电话(可能会给自己打). call(i, j) 表示第i只熊给第j只熊打电话的次数,也就是第j个串在第i个 ...
- c++(快速排序)
快速排序是编程中经常使用到的一种排序方法.可是很多朋友对快速排序有畏难情绪,认为快速排序使用到了递归,是一种非常复杂的程序,其实未必如此.只要我们使用好了方法,就可以自己实现快速排序. 首先,我们复习 ...
- Spring学习日志之Spring Security配置
依赖引入 <dependency> <groupId>org.springframework.security</groupId> <artifactId&g ...
- 二 @ResponseBody用法
@ResponseBody底层是通过response.getwriter()方法将数据写回前 台 @ResponseBody @RequestMapping (value="/queryLi ...
- UEP-保存
uep的保存操作分为ajaxgrid和ajaxform两种方式 1.ajaxgrid public void storeInfoSave(){ try { //两个dataWrap 一个dataWra ...
- 【深度学习系列】CNN模型的可视化
前面几篇文章讲到了卷积神经网络CNN,但是对于它在每一层提取到的特征以及训练的过程可能还是不太明白,所以这节主要通过模型的可视化来神经网络在每一层中是如何训练的.我们知道,神经网络本身包含了一系列特征 ...
- 十二个 ASP.NET Core 例子——1.1版本 EF MySql快速搭建
core1.0的时候搭建过一次mysql EF. 一大推问题.最近在core1.1 又重新搭了一次.简单搭建还挺快,没出现什么幺蛾子.总结下步骤 建立项目,例如ASP.NET Core1.1 WebA ...
- mysql 远程连接数据库的二种方法
一.连接远程数据库: 1.显示密码 如:MySQL 连接远程数据库(192.168.5.116),端口"3306",用户名为"root",密码"123 ...
- ACTION_NAME等常量 不能在模板里直接取值?
在控制器里可以取得,显示模板之前需要向模板传递. condition 下用$Think.const.MODULE_NAME 在模板里要用{$Think.ACTION_NAME}
- 003_JS基础_面向对象基础
3.1 对象 引入:在js中表示一个人的信息(name, gender, age)通过var申明三个变量,但是这样使用基本数据类型的变量,他们是互相独立的,没有联系: 此时就需要使用对象,对象是 ...