This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -555,15 +555,15 @@ function hide_canvas() {
<li>变更记录ChangeHistory</li>
</ul>
<p>项目、迭代、问题与子任务存在非常清晰的一对多组合关系,它们也构成了项目上下文的“骨架”。项目成员作为参与项目管理活动的角色,是主要业务用例的参与者。每个问题可以有多个附件,问题与子任务还可以有多个评论。每次对问题的修改与变更,都会生成一条变更记录。于是,可以快速获得如下的领域分析模型:</p>
<p><img src="assets/5dc5ee80-3614-11ea-b651-9bf55e9590d3" alt="50958028.png" /></p>
<p><img src="assets/5dc5ee80-3614-11ea-b651-9bf55e9590d3" alt="png" /></p>
<p>领域分析模型除了包含主要的领域概念之外,还将一些主要的属性定义为领域类,同时确定了它们之间的关系。由于一个问题只能指定一个报告人和一个经手人,因此 Issue 与 TeamMember 之间的关系是一对二的关系。</p>
<h3>领域设计模型</h3>
<p>要获得项目上下文的领域设计模型,仍然可以采用庖丁解牛的过程进行模型的细化。</p>
<p>首先理顺对象图明确各个类之间的关系。项目上下文各个类之间的关系非常清晰很容易辨别类之间的面向对象合成或聚合关系。区分合成和聚合只需判断主类是否必须拥有从类的属性值。例如Issue 必须指定 IssueType 和 IssueStatus因此是合成关系但它未必需要划分 SubType也未必一定拥有 Comment 与 Attachment它们之间的关系就是聚合关系。由于需求要求一个项目必须定义至少一个迭代如果没有手动创建迭代系统会默认创建待办项迭代因此 Project 与 Iteration 之间的关系是合成关系。</p>
<p>确定实体还是值对象也非常容易。由于业务需求的主要领域概念都需要身份标识来辨别其身份,故而定义为实体。至于这些实体的属性多数被定义为值对象,因为它们代表的领域概念只需要关心其值,无需身份标识,如 StoryPoint、IssueType 等类。由此可获得梳理后的领域对象图:</p>
<p><img src="assets/fb8fcd60-3615-11ea-b5d4-6937111bfc43" alt="48637403.png" /></p>
<p><img src="assets/fb8fcd60-3615-11ea-b5d4-6937111bfc43" alt="png" /></p>
<p>既然这些类之间的关系要么是合成关系要么是聚合关系通过分解关系薄弱处来划定聚合边界也变得非常容易。但是需要注意两点特殊之处。其一Project 和 Issue 与 TeamMember 之间都存在合成关系,且 TeamMember 是一个实体由于实体不能被两个聚合同时调用因此只能将这三个实体定义为三个独立的聚合。其二Issue 与 StoryPoint 之间的关系虽然是面向对象的聚合关系,按照依赖强弱,可以考虑将 StoryPoint 与 Issue 分开,但由于 StoryPoint 是值对象,不能独立定义为一个聚合,只能划到 Issue 实体的边界内:</p>
<p><img src="assets/0e848e10-3616-11ea-996b-ef6591d33435" alt="48727830.png" /></p>
<p><img src="assets/0e848e10-3616-11ea-996b-ef6591d33435" alt="png" /></p>
<p>确定了初步的聚合边界之后我们需要遵循聚合设计的原则来调整已有边界。Project 与 Iteration 虽然是依赖较强的合成关系,一个项目也确实需要至少一个迭代存在,但由于 Iteration 允许调用者能够直接操作和管理迭代的生命周期,具有独立性,故而需要单独为迭代划定聚合边界。</p>
<p>Issue 与 SubTask 本身是面向对象的聚合关系,一个问题也可以没有子任务;然而,一旦问题划分了子任务,问题的状态就要受到子任务状态的约束。例如,在将问题的状态设置为 Resolved 时,必须检查该问题下所有子任务的状态是否已被设置为 Resolved子任务的状态必须与问题的状态保持一致这实际上是 Issue 与 SubTask 之间存在的不变量Invariant</p>
<p>Issue 与 SubTask 之间的不变量带来了聚合设计的一个分歧。若依据不变量原则,这两个实体应放在同一个聚合中。但是,问题与子任务又都可以添加评论,由于 Comment 是一个单独的聚合,若要表示子任务与评论之间的关系,又该如何表达呢?毕竟,此时的 SubTask 只是 Issue 聚合内部的实体,它的 ID 不能暴露给当前聚合外的其他聚合。这就是聚合设计的为难之处。我们能选择的方案有以下三种:</p>
@@ -574,7 +574,7 @@ function hide_canvas() {
</ul>
<p>我们需要评估哪一个方案带来的优势更大哪一个方案带来的问题更少。A 方案将 SubTask 放在 Issue 聚合之外,意味着调用者可以通过 SubTask 的资源库单独管理子任务的生命周期,在没有 Issue 边界的控制下,很难保证 Issue 与 SubTask 之间状态的一致。B 方案会形成一个粒度较大的聚合,且 Comment 的管理只能通过 Issue 聚合根实体进行无法单独管理存在不便。C 方案满足了 Issue 与 SubTask 的不变量,也满足了 Comment 的独立性,但在一定程度上破坏了聚合边界的封装性。</p>
<p>每个方案都有自己的问题相比较而言C 方案带来的优势更大。在无法改变业务需求的情况下,我更倾向于这一方案:</p>
<p><img src="assets/38e87400-3616-11ea-a962-5985f456c479" alt="50868838.png" /></p>
<p><img src="assets/38e87400-3616-11ea-a962-5985f456c479" alt="png" /></p>
<p>聚合根实体分别为Project、Iteration、Issue、TeamMember、Comment、Attachment 与 ChangeHistory。图中仍然用面向对象的合成或聚合表现聚合根之间的关系但在设计时上游聚合根应通过 ID 与下游聚合根建立关联关系。比较特殊的是 Issue 聚合根,它需要提供 IssueId 与 SubTaskId 和下游聚合 Comment 建立关联。</p>
<p>通过用例可以确定业务场景,并利用场景驱动设计细化领域设计模型。例如“分配问题给项目成员”领域场景,可以分解任务为:</p>
<ul>
@@ -601,7 +601,7 @@ function hide_canvas() {
</li>
</ul>
<p>根据角色构造型进行职责分配获得时序图如下所示:</p>
<p><img src="assets/5dae0ed0-3616-11ea-996b-ef6591d33435" alt="58998764.png" /></p>
<p><img src="assets/5dae0ed0-3616-11ea-996b-ef6591d33435" alt="png" /></p>
<p>这一领域场景看似简单,但它实际上牵涉到项目上下文多个领域对象,以及与 OA 集成上下文、员工上下文之间的协作。其中EmployeeClient 与 NotificationClient 是针对员工上下文与 OA 集成上下文定义的南向网关(属于防腐层)。该场景的时序图脚本如下所示:</p>
<pre><code>IssueAppService.assign(issueId, owner) {
IssueService.assign(issueId, owner) {