如前所述,领域驱动设计中的业务逻辑分为两部分(层):领域逻辑和应用逻辑:
- 领域逻辑由系统的核心领域规则组成,应用逻辑实现应用特定的用例
多个应用程序层虽然定义很明确,但实现起来可能并不容易。您可能无法决定哪些代码应该位于应用程序层,哪些代码应该位于领域层。本节试图解释其中的差异
当系统比较大时,DDD有助于处理复杂性。特别是,如果在一个领域中开发了多个应用程序,那么领域逻辑与应用程序逻辑的分离就变得重要得多。
假设您正在构建一个具有多个应用程序的系统
-
一个网站应用程序,用 ASP.NET Core MVC 构建,向用户展示你的产品。这样的网站不需要认证就可以看到产品。用户只有在执行某些操作(比如将产品添加到购物车中)时才会登录到网站。
-
一个后台管理程序,使用 Angular UI 构建(使用REST APIs)。本应用被公司办公人员使用来管理系统(如编辑产品描述)
-
一个移动应用程序, 与网站相比,它具有更简单的UI。它可以通过 REST APIs 或其他技术(如TCP套接字)与服务器通信。
每个应用程序都有不同的需求、不同的用例(应用服务方法)、不同的dto、不同的验证和授权规则……等
如果将所有这些逻辑混合到单个应用程序层会使您的服务包含太多的逻辑,使代码更难开发、维护和测试,并导致潜在的bug
如果一个领域有多个应用程序:
- 为每个应用程序/客户端类型创建单独的应用程序层,并在这些单独的层中实现应用程序特定的业务逻辑。
- 使用单个领域层共享核心领域逻辑。
这样的设计使得区分领域逻辑和应用程序逻辑变得更加重要。
为了更清楚地了解实现,您可以为每个应用程序类型创建不同的项目(.csproj)。例如:
-
后台管理应用:
IssueTracker.Admin.Application & IssueTracker.Admin.Application.Contracts
-
公共网站应用:
IssueTracker.Public.Application & IssueTracker.Public.Application.Contracts
-
移动应用:
IssueTracker.Mobile.Application & IssueTracker.Mobile.Application.Contracts
本节包含一些应用程序服务和领域服务示例,以讨论如何决定将业务逻辑放置在这些服务中
示例:在领域服务中新建组织public class OrganizationManager : DomainService
{
//省略了依赖注入
public async Task<Organization> CreateAsync(string name)
{
if(await _organizationRepository.AnyAsync(x => x.Name == name))
{
throw new BusinessException("IssueTracking:DuplicateOrganizationName");
}
await _authorizationService.CheckAsync("OrganizationCreationPermissin");
Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}");
var organization = new Organization();
await _emailSender.SendAsync(
"admin@issuetracking.com",
"New Organization",
"A new organization created with name: " + name
);
return organization;
}
}
让我们一步一步地看看 CreateAsync
方法,来讨论代码部分是否应该放在领域服务中
-
正确: 它首先检查重复的组织名称,并在这种情况下抛出异常。这与核心领域规则有关,我们不允许重复名称
-
错误: 领域服务不应该执行授权。授权 应该在应用层中完成。
-
错误: 它记录了包含当前用户的用户名的消息。领域服务不应该依赖于当前用户。即使系统中没有用户,领域服务也应该可用。当前用户(会话)应该是一个与表示/应用层相关的概念。
-
错误: 它发送了关于这个新组织创建的 电子邮件。我们认为这也是一个特定于用例的业务逻辑。您可能希望在不同的用例中创建不同类型的电子邮件,或者在某些情况下不需要发送电子邮件。
public class OrganizationAppService : ApplicationService
{
//省略了依赖注入
[UnitOfWork]
[Authorize("OrganizationCreationPermissin")]
public async Task<Organization> CreateAsync(CreateOrganizationDto input)
{
await _paymentService.ChargeAsync(
CurrentUser.Id,
GetOrganizationPrice()
);
var organization = await _organizationManager.CreateAsync(input.Name);
await _organizationManager.InsertAsync(organization);
await _emailSender.SendAsync(
"admin@issuetracking.com",
"New Organization",
"A new organization created with name: " + input.Name
);
return organization; //!!!
}
private double GetOrganizationPrice()
{
return 42.0; //或者从其他地方获取
}
}
让我们一步一步地看看 CreateAsync
方法,来讨论代码部分是否应该放在应用程序服务中
-
正确: 应用程序服务方法应该是工作单元(事务)。ABP的 工作单元 系统使这个自动完成(甚至不需要为应用服务添加
[UnitOfWork]
属性)。 -
正确: 授权 应该在应用层完成。这里,它是通过使用
[Authorize]
属性来完成的 -
正确: 调用支付(基础设施服务)来为该操作收费(创建组织在我们的业务中是一种付费服务)
-
正确: 应用服务方法负责将更改保存到数据库。
-
正确: 我们可以发送 电子邮件 通知系统管理员
-
错误: 不要从应用程序服务返回实体。而是返回一个DTO。
您可能想知道为什么支付代码不在 OrganizationManager
中。这是一件很重要的事情,我们不想错过付款。
然而,仅仅重要还不足以将代码视为核心业务逻辑。 我们可能还有其他的用例,在这些用例中创建一个新的 Organization
是不需要收费的。比如:
-
管理员用户可以使用后台应用程序创建新的组织,而无需支付任何费用
-
后台工作的数据导入/集成/同步系统也可能需要创建没有任何支付操作的组织。
示例: 增删改查 操作如您所见,支付不是创建有效组织的必要操作。它是特定于用例的应用程序逻辑。
public class IssueAppService
{
private readonly IssueManager _issueManager;
public IssueAppService(IssueManager issueManager)
{
_issueManager = issueManager;
}
public async Task<IssueDto> GetAsync(Guid id)
{
return _issueManager.GetAsync(id);
}
public async Task CreateAsync(IssueCreationDto input)
{
return _issueManager.CreateAsync(input);
}
public async Task UpdateAsync(UpdateIssueDto input)
{
return _issueManager.UpdateAsync(input);
}
public async Task DeleteAsync(Guid id)
{
return _issueManager.DeleteAsync(id);
}
}
这个应用程序服务本身不做任何事情,而是将所有工作委托给领域服务。它甚至将 dto 传递给 IssueManager
- 不要仅仅为没有任何领域逻辑的简单CRUD操作创建领域服务。
- 永远不要向领域服务传递dto或从领域服务返回dto。
应用程序服务可以直接使用存储库来查询、创建、更新或删除数据,除非在这些操作期间需要执行一些领域逻辑。在这种情况下,创建 Domain Service 方法,但只针对那些真正需要的方法
如果你对领域驱动设计和构建大型企业系统更感兴趣,推荐以下书籍作为参考书:
- "Domain Driven Design" by Eric Evans
- "Implementing Domain Driven Design" by Vaughn
Vernon - "Clean Architecture" by Robert C. Martin
完结