草庐IT

实现领域驱动设计 - 使用ABP框架 - 领域逻辑 & 应用逻辑

Broadm 2023-03-28 原文

领域逻辑 & 应用逻辑

如前所述,领域驱动设计中的业务逻辑分为两部分(层):领域逻辑和应用逻辑:

  • 领域逻辑由系统的核心领域规则组成,应用逻辑实现应用特定的用例

虽然定义很明确,但实现起来可能并不容易。您可能无法决定哪些代码应该位于应用程序层,哪些代码应该位于领域层。本节试图解释其中的差异

多个应用程序层

当系统比较大时,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

完结

有关实现领域驱动设计 - 使用ABP框架 - 领域逻辑 & 应用逻辑的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  9. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  10. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

随机推荐