DDD cargo事例

货物运输系统概述

我们要为一个货运公司开发新软件。最初有3个基本功能
1. 跟踪顾客货物的关键装卸事件
2. 对货物进行事先预约
3. 当货物在装卸中抵达某个点时,自动向顾客发送发票


DDD cargo事例_第1张图片

这个模型组织了领域知识,并为开发团队提供了一种语言。我们可以给出如下陈述:
1. 多个cunsotmer涉及到一个Cargo,每个Customer充当一种不同的role。
2. 已经指定了提供目标
3. 满足Specification的一系列Carrier Movement将实现提货目标

模型中的每个对象都有清晰的含义:
1. Handling Event装卸事件是发生在Cargo上的一种离散活动,例如货物装船或者出关。这个类也许可以细分为一个包含不同种类事件的层次结构,例如装货,卸货,或者收货人提货等。

2. Delivery Specification提货规格定义了一个提货目标,它至少包含一个目的地和一个抵达时间,但是还可以更加复杂。这个类遵循规格模式(参加第9章)

3. Delivery Specification的职责本来可以由Cargo对象来承担,但是把它抽象出来至少可以有3个好处:
    a.如果没有Delivery Specification,所有与指定提货目标有关的属性和关联就都要由         Cargo对象来负责。这会使Cargo变得散乱,理解或修改会更困难。
    b.当把模型作为一个整体来说明时,这种抽象使我们可以很容易且安全地把细节隐藏起来。例如,Delivery Specification可能还封装了其他的标准,但是在这个细节层次上,我们并不需要把它暴露出来。这个图是告诉读者,这里有一个提货规格,但是它的细节并不重要,无需考虑。
    c.这个模型更富有表达能力。添加Delivery Specification把问题说得很明白了:运输Cargo的实际方式可以自由确定,但是它必须达到由Delivery Specification给出目标。

4. role将顾客在一次运输中扮演的不同角色区分开来。例如,有“托运人”、“收货人”等。对于一件特定的Cargo,一个顾客只能扮演一个角色,因此Customer和Cargo之间的关联成为限定的多对一关联,而不是多对多关联。角色可以实现为一个简单的字符串,如果还需要其他行为的话,也可以实现为一个类。

5. Carrier Movement代表一个特定的Carrier(例如一辆卡车或者一艘货轮)从一个Location到另一个地点的转移。Cargo就可以装上Carrier, 在一个或者多个Carrier Movement期间,从一个地方转移到另一个地方。

6. Delivery Specification描述目标,而Delivery History(提货历史)则反映了发生在Cargo上的实际行为。通过分析最后一次装卸或卸货对应的Carrier Movement的目的地,我们可以从Delivery History对象计算出Cargo的当前位置。如果提货成功的话,那么Delivery History最后应该满足Delivery Specification的目标。、


区分实体和值对象

1. Cunstomer
我们从一个容易一些的对象开始。一个Customer对象代表一个人活着一个公司,在通常意义下这是一个实体。

2. Cargo
两个同样的货箱必须区分,因此Cargo货物对象是实体。

3. Handling Event(装卸事件)和Carrier Movement(承运人运输)
我们关心每个个体事件,因为我们需要这些信息来跟踪进展。这些事件反映了现实世界中发生的事件,通常是不可互换的,因此它们是实体。每个Carrier Movement都会从运输计划中得到一个代码作为标识。

4. Location(地点)
名称相同的两个地点不是同一个地点。

5. Delivery History
Delivery History有些复杂。Delivery History是不可互换的,因此它们是实体。但是Delivery History与它发运的Cargo具有一对一关联,因此它实际上并没有自己的标识。它的标识是从所属的Cargo借用过来的。用聚合来建模将使问题更加清晰。

6. Delivery Specification(提货规格)
虽然Delivery Specification代表Cargo的目标,但是这个抽象并不依赖于Cargo。它实际上描述了某些Delivery History的假象状态。我们希望与一件Cargo连接的Delivery History最终能够满足与它连接的Delivery Specification;但是,虽然二者的历史在最开始时都相同,但它们不能共享同一个Delivery History。Delivery Specification是值对象。

7. Role(角色)和其他属性
Role角色提供了与其限定的关联有关的信息,但是它没有历史的或连续的要求。它是一个值对象,可以在不同的Cargo/Customer关联中进行共享。

其他属性,如时间戳或者名字,都是值对象。

聚合的边界


DDD cargo事例_第2张图片

选择仓储

设计中有5个实体是聚合根。我们只需考虑这几个对象,因为其他对象都不允许有仓储。

为了决定哪些候选者确实需要仓储,我们必须重温一下应用需求。为了在Booking Application中进行预约,用户需要选择不同角色(托运人、收货人等)的Customer。因此,我们需要一个Customer Repository。用户还要查找Location来指定Cargo的目的地,因此Location Repository也是需要的。

Activity Logging Application需要允许用户查找装载了给定Cargo的Carrier Movement,因此我们需要一个Carrier Movement Repository。用户需要告诉系统哪个Cargo已经装载,因此还需要一个Cargo Repository。

DDD cargo事例_第3张图片

对象的创建

我们将每个要创建的对象放到工厂里,然后传入需要的参数,然后开始创建。
遗憾的是,故事并不像这么简单。Cargo到Delivery History到Handling Event再回到Cargo的这个引用循环把事例创建问题搞复杂了。Delivery History持有一个与其Cargo相关的Handling Event集合,而新创建的Handling Event对象必须在同一个事物中加入到这个集合中来。如果不创建这种反向指针,对象将使不一致的,如下图:

DDD cargo事例_第4张图片
我们会发现一个动作会牵扯到两个操作,那么这个动作就是事物的,这样有出现一定的问题,下面我们给出另一种解决办法。

实际上,当把数据库作为底层技术时,我们可以设法用内部执行的查询来模拟对象集合。使用查询而不是集合使我们能更容易地维护Cargo和Handling Event之间的循环引用的一致性。

我们为Handling Event增加一个仓储来承担查询的职责。Handling Event Repository将提供一个根据给定Cargo查询其相关Event的功能。此外,仓储还可以为某些特定的问题进行优化,使其获得更高的查询效率。例如,为了推断Cargo的当前状态,我们需要根据Delivery History来查找最后一次报告的装货和卸货。如果这个访问路径使用非常频繁,那么我们可以设计一个专门的查询,仅仅返回与最后一次报告相关的Handling Event。如果我们希望用一个查询来得出某个给定Carrier Movement中装载的所有Cargo,也只要增加一个查询就行了。

你可能感兴趣的:(Go)