值对象的不变性使得程序的逻辑变得更加简单,你不用去维护复杂的状态信息,需要的时候创建,不要的时候直接扔掉即可,使得值对象就像程序中的过客一样。在DDD建模中,一种受推崇的做法便是将业务概念尽量建模为值对象。
对于OrderItem来说,由于我们的业务需要对OrderItem的数量进行修改,也即拥有生命周期的意味,因此本文将OrderItem建模为了实体对象。但是,如果没有这样的业务需求,那么将OrderItem建模为值对象应该更合适一些。
另外,需要指明的是,实体和值对象的划分并不是一成不变的,而应该根据所处的限界上下文来界定,相同一个业务名词,在一个限界上下文中可能是实体,在另外的限界上下文中可能是值对象。比如,订单Order在采购上下文中应该建模为一个实体,但是在物流上下文中便可建模为一个值对象。
聚合根的家——资源库通俗点讲,资源库(Repository)就是用来持久化聚合根的。从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型。另外,在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。
实现Order的资源库OrderRepository如下:
public void save(Order order) { String sql = "INSERT INTO ORDERS (ID, JSON_CONTENT) VALUES (:id, :json) " + "ON DUPLICATE KEY UPDATE JSON_CONTENT=:json;"; Map<String, String> paramMap = of("id", order.getId().toString(), "json", objectMapper.writeValueAsString(order)); jdbcTemplate.update(sql, paramMap); } public Order byId(OrderId id) { try { String sql = "SELECT JSON_CONTENT FROM ORDERS WHERE ID=:id;"; return jdbcTemplate.queryForObject(sql, of("id", id.toString()), mapper()); } catch (EmptyResultDataAccessException e) { throw new OrderNotFoundException(id); } }在OrderRepository中,我们只定义了save()和byId()方法,分别用于保存/更新聚合根和通过ID获取聚合根。这两个方法是Repository中最常见的方法,有的DDD实践者甚至认为一个纯粹的Repository只应该包含这两个方法。
读到这里,你可能会有些疑问:为什么OrderRepository中没有更新和查询等方法?事实上,Repository所扮演的角色只是向领域模型提供聚合根而已,就像一个聚合根的“容器”一样,这个“容器”本身并不关心客户端对聚合根的操作到底是新增还是更新,你给一个聚合根对象,Repository只是负责将其状态从计算机的内存同步到持久化机制中,从这个角度讲,Repository只需要一个类似save()的方法便可完成同步操作。当然,这个是从概念的出发点得出的设计结果,在技术层面,新增和更新还是需要区别对待,比如SQL语句有insert和update之分,只是我们将这样的技术细节隐藏在了save()方法中,客户方并无需知道这些细节。在本例中,我们通过MySQL的ON DUPLICATE KEY UPDATE特性同时处理对数据库的新增和更新操作。当然,我们也可以通过编程判断聚合根在数据库中是否已经存在,如果存在则update,否则insert。另外,诸如Hibernate这样的持久化框架自动提供saveOrUpate()方法可以直接用于对聚合根的持久化。
对于查询功能来说,在Repository中实现查询本无不合理之处,然而项目的演进可能导致Repository中充斥着大量的查询代码“喧宾夺主”似的掩盖了Repository原本的目的。事实上,DDD中读操作和写操作是两种很不一样的过程,笔者的建议是尽量将此二者分开实现,由此查询功能将从Repository中分离出去,在下文中我将详细讲到。
在本例中,我们在技术实现上使用到了Spring的JdbcTemplate和JSON格式持久化Order聚合根,其实Repository并不与某种持久化机制绑定,一个被抽象出来的Repository向外暴露的功能“接口”始终是向领域模型提供聚合根对象,就像“聚合根的家”一样。
好了,至此让我们来做个回顾,上文中我们以“更新Order中的Product数量”业务需求为例,讲到了应用服务、聚合根和资源库,对该业务需求的处理流程体现了DDD处理业务需求的最常见最典型的形式:
应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根中的业务方法,最后再次调用资源库保存聚合根。
流程示意图如下:
创生之柱——工厂稍微提炼一下,我们便知道软件里面的写操作要么是修改既有数据,要么是新建数据。对于前者,DDD给出的答案已经在上文中讲到,接下来我们讲讲在DDD中如何新建聚合根。