当前位置: 首页 > news >正文

网站建设最好的公司哪家好做网站的顶部图片

网站建设最好的公司哪家好,做网站的顶部图片,黄页推广软件网站,上海浦东网站设计公司前言上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案、项目组成、项目引用关系#xff0c;以及基于ABP落地DDD的通用原则。从这本篇开始#xff0c;会更加深入地介绍在基于 ABP Framework 落地DDD过程中的最佳实践和原则。围绕DDD和ABP Framework两… 前言上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案、项目组成、项目引用关系以及基于ABP落地DDD的通用原则。从这本篇开始会更加深入地介绍在基于 ABP Framework 落地DDD过程中的最佳实践和原则。围绕DDD和ABP Framework两个核心技术后面还会陆续发布核心构件实现、综合案例实现系列文章敬请关注 ABP Framework 研习社QQ群726299208 ABP Framework 学习及实施DDD经验分享示例源码、电子书共享欢迎加入领域对象是DDD的核心我们会依次分析聚合/聚合根、仓储、规约、领域服务的最佳实践和规则。内容较多会拆分成多个章节单独展开。本文重点讨论领域对象——聚合和聚合根的最佳实践和原则首先我们需要一个业务场景例子中会用到 GitHub 的一些概念如Issue建议、Repository代码仓库、Label标签和User用户。下图显示了业务场景对应的聚合、聚合根、实体、值对象以及它们之间的关系。Issue 聚合是由 Issue聚合根、Comment实体和 IssuelLabel值对象组成的集合。因为其他聚合相对简单所以我们重点分析 Issue 聚合。聚合正如前面所讲一个聚合是一系列对象实体和值对象的集合通过聚合根将所有关联对象绑定在一起。本节将介绍与聚合相关的最佳实践和原则。我们对聚合根和子集合实体都使用实体这个术语除非明确写出聚合根或子集合实体。聚合和聚合根原则包含业务原则•实体负责实现与其自身属性相关的业务规则。•聚合根还负责其子集合实体状态管理。•聚合应该通过实现领域规则和规约来保持自身的完整性和有效性。这意味着与数据传输对象DTO不同实体具有实现业务逻辑的方法。实际上我们应该尽可能在实体中实现业务规则。单个单元原则聚合及其所有子集合作为单个单元被检索和保存。例如如果向 Issue 添加 Comment需要这样做•从数据库中获取 Issue 包含所有子集合Comments 该问题的评论列表 和 IssueLabels 该问题的标签集合。•在 Issue 类中调用方法添加一个新的 Comment比如 Issue.AddCommnet(...)•作为一个单一的数据库更新操作将 Issue包括所有子集合保存到数据库。对于习惯使用 EF Core 和 关系数据的开发者来说这看起来似乎有些奇怪。获取 Issue 的所有数据是没有必要且低效的。为什么我们不直接执行一个SQL插入命令到数据库而不查询任何数据呢答案是我们应该在代码中实现业务规则并保持数据的一致性和完整性。如果我们有一个业务规则如用户不能对锁定的 Issue 进行评论我们如何不通过检索数据库中数据的情况下检查 Issue 的锁定状态呢所以只有当应用程序代码中的相关对象可用时即获取到聚合及其所有子集合数据时我们才能执行该业务规则。另一方面MongoDB开发者会发现这个规则非常自然。因为在 MongoDB 中一个聚合对象包括子集合被保存在数据库中的一个集合中而在关系型数据库中它被分布在数据库中几个表中。因此当你得到一个聚合时所有的子集合已经作为查询的一部分被检索出来了不需要任何额外配置。ABP框架有助于在您的应用程序中实现这一原则。示例添加 Comment 到 Issuepublic class IssueAppService : ApplicationService ,IIssueAppService {private readonly IRepositoryIssue,Guid _issueRepository;public IssueAppService(IRepositoryIssue,Guid issueRepository){_issueRepository issueRepository;}[Authorize]public async Task CreateCommentAsync(CreateCommentDto input){var issue await _issueRepository.GetAsync(input.IssueId);issue.AddComment(CurrentUser.GetId(),input.Text);await _issueRepository.UpdateAsynce(issue);} } _issueRepository.GetAsync(...)方法默认作为单个单元检索 Issue 对象并包含所有子集合。对于 MongoDB 来说这个操作开箱即用但是使用 EF Core 需要配置聚合与数据库映射配置后 EF Core 仓储实现 会自动处理。_issueRepository.GetAsync(...)方法提供一个可选参数includeDetails可以传递值 false 禁用该行为不包含子集合对象只在需要时启用它。Issue.AddComment(...)传递参数 userId 和 text 表示用户ID和评论内容添加到 Issue 的 Comments 集合中并实现必要的业务逻辑验证。最后使用 _issueRepository.UpdateAsync(...) 保存更改到数据库。EF Core 提供 变更跟踪Change Tracking功能实际上你不需要调用 _issueRepository.UpdateAsync(...) 方法会自动进行保存。这个功能是由 ABP 工作单元系统 提供应用服务的方法作为一个单独的工作单元在执行完之后会自动调用 DbContext.SaveChanges()。当然如果使用 MongoDB 数据库则需要显示地更新已经更改的实体。所以如果你想要编写独立于数据库提供程序的代码应该总是为要更改的实体调用UpdateAsync()方法。事务边界原则一个聚合通常被认为是一个事务边界。如果用例使用单个聚合读取并保存为单个单元那么对聚合对象所做的所有更改将作为原子操作保存而不需要显式地使用数据库事务。当然我们可能需要处理将多个聚合实例作为单一用例更改的场景此时需要使用数据库事务确保更新操作的原子性和数据一致性。正因为如此ABP框架为一个用例即一个应用程序服务方法显式地使用数据库事务一个应用程序服务方法就是一个工作单元。可序列化原则聚合包含根实体和子集合应该是可序列化的并且可以作为单个单元在网络上进行传输。举个例子MongoDB序列化聚合为Json文档保存到数据库反序列化从数据库中读取的Json数据。当您使用关系数据库和ORM时没有必要这样做。然而它是领域驱动设计的一个重要实践。聚合和聚合根最佳实践以下最佳实践确保实现上述原则。只通过ID引用其他聚合一个聚合应该只通过其他聚合的ID引用聚合这意味着你不能添加导航属性到其他聚合。•这条规则使得实现可序列化原则得以实现。•可以防止不同聚合相互操作以及将聚合的业务逻辑泄露给另一个聚合。我们来看一个例子两个聚合根GitRepository 和 Issue public class GitRepository:AggregateRootGuid {public string Name {get;set;}public int StarCount{get;set;}public CollectionIssue Issues {get;set;} //错误代码示例 }public class Issue:AggregateRootGuid {public tring Text{get;set;}public GitRepository Repository{get;set;} //错误代码示例public Guid RepositoryId{get;set;} //正确示例 } •GitRepository 不应该包含 Issue 集合他们是不同聚合。•Issue 不应该设置导航属性关联 GitRepository 因为他们是不同聚合。•Issue 使用 RepositoryId 关联 Repository 聚合正确。当你有一个 Issue 需要关联的 GitRepository 时那么可以从数据库通过 RepositoryId 直接查询。用于 EF Core 和 关系型数据库在 MongoDB 中自然不适合有这样的导航属性/集合。如果这样做在源集合的数据库集合中会保存目标集合对象的副本因为它在保存时被序列化为JSON这样可能会导致持久化数据的不一致。然而EF Core 和关系型数据库的开发者可能会发现这个限制性的规则是不必要的因为 EF Core 可以在数据库的读写中处理它。但是我们认为这是一条重要的规则有助于降低领域的复杂性防止潜在的问题我们强烈建议实施这条规则。然而如果你认为忽略这条规则是切实可行的请参阅前面基于ABP落地领域驱动设计-01.全景图[2]中关于数据库独立性原则的讨论部分。保持聚合根足够小一个好的做法是保持一个简单而小的聚合。这是因为一个聚合体将作为一个单元被加载和保存读/写一个大对象会导致性能问题。请看下面的例子public class UserRole:ValueObject {public Guid UserId{get;set;}public Guid RoleId{get;set;} }public class Role:AggregateRootGuid {public string Name{get;set;}public CollectionUserRole Users{get;set;} //错误示例角色对应的用户是不断增加的 } public class User:AggregateRootGuid {public string Name{get;set;}public CollectionUserRole Roles{get;set;}//正确示例一个用户拥有的角色数量是有限的 } Role聚合 包含 UserRole 值对象集合用于跟踪分配给此角色的用户。注意UserRole 不是另一个聚合对于规则仅通过Id引用其他聚合没有冲突。然而实际却存在一个问题。在现实生活中一个角色可能被分配给数以千计甚至数以百万计的用户每当你从数据库中查询一个角色时加载数以千计的数据项是一个重大的性能问题。记住聚合是由它们的子集合作为一个单一单元加载的。另一方面用户可能有角色集合因为实际情况中用户拥有的角色数量是有限的不会太多。当您使用用户聚合时拥有一个角色列表可能会很有用且不会影响性能。如果你仔细想想当使用非关系型数据库如MongoDB时当Role和User都有关系列表时还有一个问题在这种情况下相同的信息会在不同的集合中重复出现将很难保持数据的一致性每当你在User.Roles中添加一个项你也需要将它添加到Role.Users中。因此根据以下因素来确定聚合边界和大小•考虑对象关联性是否需要在一起使用。•考虑性能查询加载/保存性能和内存消耗。•考虑数据的完整性、有效性和一致性。而实际•大多数聚合根没有子集合。•一个子集合最多不应该包含超过100-150个条目。如果您认为集合可能有更多项时请不要定义集合作为聚合的一部分应该考虑为集合内的实体提取为另一个聚合根。聚合根/实体中的主键•一个聚合根通常有一个ID属性作为其标识符主键Primark Key: PK。推荐使用 Guid 作为聚合根实体的PK。•聚合中的实体不是聚合根可以使用复合主键。示例聚合根和实体//聚合根单个主键 public class Organization {public Guid Id{get;set;}public string Name{get;set;}//... } //实体复合主键 public class OrganizationUser {public Guid OrganizationId{get;set;} //主键public Guid UserId{get;set;}//主键public bool IsOwner{get;set;}//... } •Organization 包含 Guid 类型主键 Id•OrganizationUser 是 Organization 中的子集合有复合主键OrganizationId 和 UserId 。这并不意味着子集合实体应该总是有复合主键只有当需要时设置通常是单一的ID属性。复合主键实际上是关系型数据库的一个概念因为子集合实体有自己的表需要一个主键。另一方面例如在MongoDB中你根本不需要为子集合实体定义主键因为它们是作为聚合根的一部分来存储的。聚合根/实体构造函数构造函数是实体的生命周期开始的地方。一个设计良好的构造函数担负以下职责•获取所需的实体属性参数来创建一个有效的实体。应该强制只传递必要的参数并可以将非必要的属性作为可选参数。•检查参数的有效性。•初始化子集合。示例Issue聚合根构造函数using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Volo.Abp; using Volo.Abp.Domain.Entities;namespace IssueTracking.Issues {public class Issue:AggregateRootGuid{public Guid RepositoryId{get;set;}public string Title{get;set;}public string Text{get;set;}public Guid? AssignedUserId{get;set;}public bool IsClosed{get;set;}pulic IssueCloseReason? CloseReason{get;set;} //枚举public ICollectionIssueLabel Labels {get;set;}public Issue(Guid id,Guid repositoryId,string title,string textnull,Guid? assignedUserId null):base(id){//属性赋值RepositoryIdrepositoryId;//有效性检测TitleCheck.NotNullOrWhiteSpace(title,nameof(title));Texttext;AssignedUserIdassignedUserId;//子集合初始化Labelsnew CollectionIssueLabel();}private Issue(){/*反序列化或ORM 需要*/}} } •Issue类通过其构造函数参数获得属性所需的值以此创建一个正确有效的实体。•在构造函数中验证输入参数的有效性比如Check.NotNullOrWhiteSpace(...) 当传递的值为空时抛出异常ArgumentException。•初始化子集合当使用 Labels 集合时不会获取到空引用异常。•构造函数将参数id传递给base类不在构造函数中生成 Guid可以将其委托给另一个 Guid生成服务作为参数传递进来。•无参构造函数对于ORM是必要的。我们将其设置为私有以防止在代码中意外地使用它。实体属性访问器和方法上面的示例代码看起来可能很奇怪。比如在构造函数中我们强制传递一个不为null的Title。但是我们可以将 Title 属性设置为 null而对其没有进行任何有效性控制。这是因为示例代码关注点暂时只在构造函数。如果我们用 public 设置器声明所有的属性就像上面的Issue类中的属性例子我们就不能在实体的生命周期中强制保持其有效性和完整性。所以•当需要在设置属性时执行任何逻辑请将属性设置为私有private。•定义公共方法来操作这些属性。示例通过方法修改属性namespace IssueTracking.Issues {public Guid RepositoryId {get; private set;} //不更改public string Title { get; private set; } //更改需要非空验证public string Text{get;set;} //无需验证public Guid? AssignedUserId{get;set;} //无需验证public bool IsClosed { get; private set; } //需要和 CloseReason 一起更改public IssueCloseReason? CloseReason { get;private set;} //需要和 IsClosed 一起更改public class Issue:AggregateRootGuid{//...public void SetTitle(string title){TitleCheck.NotNullOrWhiteSpace(title,nameof(title));}public void Close(IssueCloseReason reason){IsClosed true;CloseReason reason;}public void ReOpen(){IsClosedfalse;CloseReasonnull;}} } •RepositoryId 设置器设置为私有private因为 Issue 不能将 Issue 移动到另一个 Repository 中该属性创建之后无需更改。•Title 设置器设置为私有当需要更改时可以使用 SetTitle 方法这是一种可控的方式。•Text 和 AssignedUserId 都有公共设置器因为这两个字段并没有约束可以是null或任何值。我们认为没有必要定义单独的方法来设置它们。如果以后需要可以添加更改方法并将其设置器设置为私有。领域层是内部项目并不会暴露给客户端使用所以这种更改不会有问题。•IsClosed 和 IssueCloseReason 是成对修改的属性分别定义 Close 和 ReOpen 方法一起修改他们。通过这种方式可以防止在没有任何理由的情况下关闭一个问题。业务逻辑和实体中的异常处理当你在实体中进行验证和实现业务逻辑经常需要管理异常•创建特定领域异常。•必要时在实体方法中抛出这些异常。示例public class Issue:AggregateRootGuid {//..public bool IsLocked {get;private set;}public bool IsClosed{get;private set;}public IssueCloseReason? CloseReason {get;private set;}public void Close(IssueCloseReason reason){IsClose true;CloseReason reason;}public void ReOpen(){if(IsLocked){throw new IssueStateException(不能打开一个锁定的问题请先解锁);}IsClosedfalse;CloseReasonnull;}public void Lock(){if(!IsClosed){throw new IssueStateException(不能锁定一个关闭的问题请先打开);}}public void Unlock(){IsLocked false;} } 这里有两个业务规则•锁定的Issue不能重新打开•不能锁定一个关闭的IssueIssue 类在这些业务规则中抛出异常 IssueStateException 。namespace IssueTracking.Issues {public class IssueStateException : Exception{public IssueStateException(string message):base(message){}} } 抛出此类异常有两个潜在问题:1.在这种异常情况下终端用户是否应该看到异常(错误)消息如果是如何实现本地化异常消息因为不能在实体中注入和使用IStringLocalizer导致不能使用本地化系统。2.对于 Web 应用程序或 HTTP API应该给客户端返回什么 HTTP Status CodeABP框架 Exception Handing 系统处理了这些问题。示例抛出业务异常using Volo.Abp; namespace IssuTracking.Issues {public class IssueStateException : BuisinessException{public IssueStateExcetipn(string code): base(code){}} } •IssueStateException 类继承 BusinessException 类。ABP框架在请求禁用时默认返回 403 HTTP 状态码发生内部错误是返回 500 HTTP 状态码。•code 用作本地化资源文件中的一个键用于查找本地化消息。现在我们可以修改 ReOpen 方法public void ReOpen() {if(IsLocked){throw new IssueStateException(IssueTracking:CanNotOpenLockedIssue);}IsClosedfalse;CloseReasonnull; } 建议使用常量代替魔术字符串IssueTracking:CanNotOpenLockedIssue。然后在本地化资源中添加一个条目如下所示:IssueTracking:CanNotOpenLockedIssue:不能打开一个锁定的问题请先解锁 •当抛出异常时ABP自动使用这个本地化消息(基于当前语言)向终端用户显示。•异常CodeIssueTracking:CanNotOpenLockedIssue被发送到客户端因此它可以以编程方式处理错误情况。实体中业务逻辑需要用到外部服务当业务逻辑只使用该实体的属性时在实体方法中实现业务规则是很简单的。如果业务逻辑需要查询数据库或使用任何应该从依赖注入系统中获取的外部服务时该怎么办请记住实体不能注入服务。有两个方式实现•在实体方法上实现业务逻辑并将外部依赖项作为方法的参数。•创建领域服务Domain Service领域服务在后面介绍现在让我们看看如何在实体类中实现它。示例业务规则一个用户不能同时分配超过3个未解决的问题public class Issue:AggregateRootGuid {//..public Guid? AssignedUserId{get;private set;}//问题分配方法public async Task AssignToAsync(AppUser user,IUserIssueService userIssueService){var openIssueCount await userIssueService.GetOpenIssueCountAsync(user.Id);if(openIssueCount 3 ){throw new BusinessException(IssueTracking:CanNotOpenLockedIssue);}AssignedUserIduser.Id;}public void CleanAssignment(){AssignedUserIdnull;} } •AssignedUserId 属性设置器设置为私有通过 AssignToAsync 和 CleanAssignment 方法进行修改。•AssignToAsync 获取一个 AppUser 实体实际上只用到 user.Id传递实体是为了确保参数值是一个存在的用户而不是一个随机值。•IUserIssueService 是一个任意的服务用于获取分配给用户的问题数量。如果业务规则不满足则抛出异常。所有规则满足则设置 AssignedUserId 属性值。此方法完全实现了应用业务逻辑然而它有一些问题•实体变得复杂因为实体类依赖外部服务。•实体变得难用调用方法时需要注入依赖的外部服务 IUserIssueService 作为参数。聚合和聚合根的最佳实践和原则部分完结学习帮助围绕DDD和ABP Framework两个核心技术后面还会陆续发布核心构件实现、综合案例实现系列文章敬请关注ABP Framework 研习社QQ群726299208 专注 ABP Framework 学习及DDD实施经验分享示例源码、电子书共享欢迎加入
http://wiki.neutronadmin.com/news/151351/

相关文章:

  • 签证中心网站建设免费空间 个人网站 google广告联盟
  • 怎么做类似淘宝网站吗怎样创办一个网站
  • 网站开发哪好趣乐码少儿编程加盟
  • 做网站需要哪些东西qq推广赚钱一个2元
  • 用php做网站需要什么软件asp网站发邮件
  • wordpress安装2个网站吗聊城专业网站建设公司电话
  • 做盗版音乐网站工厂网站怎么做
  • 服装设计网站有哪些推荐一个空间放两个网站
  • 浙江省建设厅网站电视台网站如何做新闻报道
  • 网站横幅广告怎么做网站首页设计素材
  • 恋爱ppt模板免费下载网站网页界面设计要中重点掌握
  • 佛山做网站哪家好做一般的公司门户网站投资额
  • 网站优化优化怎么做江苏个人网站备案
  • 如何做公司网站百度推广wordpress调用页面列表
  • 谷歌网站收录提交入口中国互联网站建设中心建站中心
  • 阜城网站建设公司seo发帖网站
  • 建设网站需要什么人员百度收录网站方法
  • 合肥做企业网站的网络公司展示型网站 数据库
  • 网站的ftp地址是什么制作简历的免费网站
  • 网站域名怎么取无锡画室网站建设
  • 哪些网站可以做直播小程序外包公司哪家好
  • php婚庆网站源码软件开发学什么专业好
  • 哪里有免费的网站模板下载 迅雷下载 迅雷下载软件如何向百度提交网站地图
  • 网站编程软件有哪些做网站用什么开发好
  • 湘潭网站建设出色磐石网络网站会员系统方案
  • 微信商城网站方案网站仿制
  • 济南行知网站建设做响应式网站最大宽度
  • 网站平台建设服务承诺书黄山旅游攻略必去景点
  • 网站开发人员职责网站建设的市场定位
  • 360网站托管西安网络科技公司