架构真意:企业级应用架构设计方法论与实践
上QQ阅读APP看书,第一时间看更新

3.3.3 聚合

要将领域模型最终转换为程序设计,仅仅有服务、实体与值对象,并进行一些贫血模型与充血模型的设计,是远远不够的,还需要有聚合、仓库与工厂的设计。

聚合是领域驱动设计中一个非常重要的概念,它表达的是真实世界中那些整体与部分的关系,如订单与订单明细、表单与表单明细、发票与发票明细。以订单为例,在真实世界中,订单与订单明细本来是同一个事物,订单明细是订单中的一个属性,但由于关系型数据库中没有办法在一个字段中表达一对多的关系,因此必须将订单明细设计成另外一张表。在领域模型的设计中,我们又将其还原到真实世界中,以“聚合”的形式进行设计。比如,将订单明细设计成订单中的一个属性,代码如下:


public class Order {
  private Set<Items> items;
  public void setItems(Set<Item> items){
    this.items = items;
  }
  public Set<Item> getItems(){
    return this.items;
  }
  ......
}

有了这样的设计,在创建订单的时候,不需要再单独创建订单明细了,而是将订单明细创建在订单中;在保存订单的时候,应当同时保存订单表与订单明细表,并放在同一事务中;在查询订单时,应当同时查询订单表与订单明细表,并将其装配成一个订单对象。这时候,订单就被作为一个整体进行操作,不再单独去操作订单明细。也就是说,对订单明细的操作是封装在订单对象内部的设计实现。对于客户程序来说,只使用订单对象就可以了,包括作为属性去访问订单对象的订单明细,而不再需要关注它内部是如何操作的。

当创建或更新订单时,在订单对象中填入或更新订单的明细即可。在保存订单时,只需要将订单对象作为整体去保存,而不需要关心订单数据是怎么保存的,保存到哪几张表中,是不是有事务。保存数据库的所有细节都封装在了订单对象内部。当删除订单时,删除订单对象就好了,删除订单明细是订单对象内部的实现,外部的程序不需要关注。当查询或装载订单时,客户程序只需要根据查询语句或ID查询订单对象就好了,查询程序会在查询过程中自动补填订单对应的订单明细。按照以上的思路进行的设计就是聚合。

聚合体现的是一种整体与部分的关系。正是因为有这样的关系,在操作整体的时候,整体就封装了对部分的操作。但并非所有对象间的关系都有整体与部分的关系,那些不是整体与部分的关系不能设计成聚合。因此,正确地识别聚合关系就变得尤为重要。

所谓的整体与部分的关系,就是当整体不存在时,部分就没有了意义。部分是整体的一个部分,与整体有相同的生命周期。比如,只有创建了这张订单,才能创建它的订单明细;如果没有了这张订单,那么它的订单明细就变得没有意义,就需要同时被删除。这样的关系才具备整体与部分的关系,才是聚合。

再比如,订单与用户之间的关系就不是聚合。这个用户不是创建订单时才存在的;而当删除订单时,用户也不会随着订单的删除而删除。

那么,饭店和菜单的关系是不是聚合关系呢?关键要看系统如何设计。如果系统设计成每个饭店都有自己各不相同的菜单,每个菜单都隶属于某个饭店,则饭店和菜单是聚合关系。这种设计中,虽然各个饭店都有“宫保鸡丁”,但每个饭店的“宫保鸡丁”都不同,有各自的描述、图片与价格,在数据库中是各不相同的记录。这时,要查询菜单要先查询饭店,离开了饭店的菜单是没有意义的。

但是,饭店和菜单还可以有另外一种设计思路,就是所有的菜单都是公用的,每个饭店只是去选择有还是没有这个菜品。这时,系统中有一个菜单对象,“宫保鸡丁”只是这个对象中的一条记录。其他各个饭店,如果它们的菜单上有“宫保鸡丁”,则去引用这个对象,没有则不引用。这时,菜单就不再是饭店的一个部分,没有这个饭店,这个菜品依然存在,此时就不再是聚合关系。因此,判断聚合关系最有效的方法,就是去探讨如果整体不存在时部分是否存在。如果不存在,就是聚合;反之,则不是。

有了聚合关系,部分就被封装在整体里面,这时候就会形成一种约束,即外部程序不能跳过整体去操作部分,对部分的操作都必须要通过整体。这时,整体就成为了外部访问的唯一入口,被称为“聚合根”。也就是说,一旦将对象间的关系设计成了聚合,那么外部程序只能访问聚合根,而不能访问聚合中的其他对象。这样的好处就是,当聚合内部的业务逻辑发生变更时,只与聚合内部有关,只需要对聚合内部进行更新,与外部程序无关,从而有效降低变更的维护成本,提高系统的设计质量。

然而,这样的设计并非所有时候都有效。譬如,在管理订单、对订单进行增删改时,聚合是有效的,但是如果要统计销量,分析销售趋势、销售占比,需要对大量的订单明细进行汇总和统计,此时每次操作都必须查询订单,必然导致效率极低而无法使用。因此,领域驱动设计通常适用于增删改的业务操作,而不适用于分析统计。增删改的业务可以采用领域驱动的设计,而在非增删改的分析汇总场景,则不必采用领域驱动的设计,直接SQL查询即可。