ABP Framework教程:图书管理系统开发(6) - 作者领域层设计
前言
在之前的教程中,我们已经学习了如何使用ABP框架的基础设施快速构建服务:
- 使用CrudAppService基类简化标准CRUD应用服务的开发
- 使用泛型仓储自动实现数据库层操作
在"作者管理"功能部分,我们将:
- 手动实现部分功能,展示在需要时的自定义开发方式
- 实践领域驱动设计(DDD)的最佳实践
本教程采用分层开发的方式,每次专注于一个特定层次。在实际项目中,我们通常会采用垂直开发方式(按功能模块开发)。通过这种方式,您可以体验两种不同的开发模式。
作者实体设计
在Acme.BookStore.Domain
项目中创建Authors
文件夹,并添加Author
类:
public class Author : FullAuditedAggregateRoot<Guid>
{
public string Name { get; private set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
private Author() { }
internal Author(
Guid id,
string name,
DateTime birthDate,
string? shortBio = null)
: base(id)
{
SetName(name);
BirthDate = birthDate;
ShortBio = shortBio;
}
internal Author ChangeName(string name)
{
SetName(name);
return this;
}
private void SetName(string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: AuthorConsts.MaxNameLength
);
}
}
设计要点解析
-
继承关系:
- 继承自
FullAuditedAggregateRoot<Guid>
,使实体具备软删除和完整审计功能 - 软删除意味着删除操作不会真正从数据库移除记录,而是标记为已删除
- 继承自
-
属性封装:
Name
属性使用private set
限制外部直接修改- 提供两种修改名称的方式:
- 构造函数中初始化
- 通过
ChangeName
方法修改
-
访问控制:
- 构造函数和
ChangeName
方法标记为internal
,限制只能在领域层使用 - 使用
AuthorManager
领域服务来管理这些操作
- 构造函数和
-
参数校验:
- 使用ABP提供的
Check
工具类进行参数验证 - 验证失败时会抛出
ArgumentException
- 使用ABP提供的
常量定义
在Acme.BookStore.Domain.Shared
项目中定义作者相关常量:
public static class AuthorConsts
{
public const int MaxNameLength = 64;
}
将常量定义在共享项目中,便于在DTO等其他层中复用。
领域服务:AuthorManager
由于Author
的构造和名称修改方法都是internal
的,我们需要创建领域服务来管理这些操作:
public class AuthorManager : DomainService
{
private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<Author> CreateAsync(
string name,
DateTime birthDate,
string? shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
}
public async Task ChangeNameAsync(
Author author,
string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
author.ChangeName(newName);
}
}
关键设计考虑
-
唯一性约束:
- 在创建和修改作者名称时检查名称是否已存在
- 确保系统中作者名称唯一
-
业务异常处理:
- 当名称冲突时抛出
AuthorAlreadyExistsException
- 使用ABP的业务异常机制,便于后续处理和本地化
- 当名称冲突时抛出
-
DDD实践建议:
- 仅在真正需要执行业务规则时才引入领域服务
- 保持领域服务的精简和专注
业务异常定义
public class AuthorAlreadyExistsException : BusinessException
{
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
}
在BookStoreDomainErrorCodes
中定义错误代码:
public static class BookStoreDomainErrorCodes
{
public const string AuthorAlreadyExists = "BookStore:00001";
}
添加本地化资源:
"BookStore:00001": "已存在同名作者: {name}"
作者仓储接口
定义IAuthorRepository
接口:
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
接口设计说明
-
基础继承:
- 继承自
IRepository<Author, Guid>
,获得标准仓储方法
- 继承自
-
自定义方法:
FindByNameAsync
:按名称查询作者GetListAsync
:获取分页、排序和过滤的作者列表
-
实际应用建议:
- 在真实项目中,可以优先使用泛型仓储提供的查询能力
- 本教程展示自定义仓储方法的实现方式,便于学习
总结
本教程完成了作者管理功能的领域层设计,主要实现了:
- 作者实体:封装业务规则,控制修改方式
- 领域服务:强制执行业务规则,维护名称唯一性
- 仓储接口:定义数据访问契约
通过分层开发的方式,我们专注于领域模型的设计和实现,为后续的应用层和数据库集成打下坚实基础。下一部分我们将实现仓储的具体逻辑和数据库集成。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考