一、前言

在上一篇中(如何一步一步用 DDD 设计一个电商网站(八)—— 会员价的集成),有一行注释的代码:

  1. public interface IRoleDiscountRelationRepository
  2. {
  3. RoleDiscountRelation Get(string roleId);
  4. }

其中涉及的到问题是关于值对象的持久化问题。是的,由于我们之前的设计中持久化是仅针对聚合根的:

  1. public interface IRepository<T> where T : AggregateRoot
  2. {
  3. string NextIdentity();
  4. void Save(T aggregate);
  5. T GetByIdentity(string identity);
  6. }

但是有时候难免会遇到一些需要持久化值对象的场景:

场景 1:一些不属于任何聚合根的对象,本身又可以当作一个不可变的值来看待(如省市区信息等),当然的确某个地区改名了可以作为一个新的值对象来表示。那么我们在把它们建立为值对象的同时,又需要持久化到数据库。这里就如这个等级折扣。

场景 2:一个聚合根的内部引用了一个值对象的集合,那么如果使用的是关系型数据库进行存储,必然需要单独存一个表。

其中场景 1 正好是我们 Demo 里遇到的场景,下面来一起阐述下我对这 2 个场景的理解和处理方式。

二、场景 1 的思考

整个问题的解决方式,首先需要梳理清楚 3 个基本概念:“聚合根”、“实体”、“值对象” 这 3 者的关系。这个我在(如何一步一步用 DDD 设计一个电商网站(二)—— 项目架构)中有提及。

因为涉及到持久化,所以我们可以再通过分析这 3 种对象的生命周期来帮助思考。

聚合根:独立存在的对象,是代表某个限界上下文中的一个高内聚的整体概念。他的生命周期是其所属上下文中所承担的职责的周期。

实体:无法独立存在,是聚合根的一部分,生命周期由所属的聚合根掌控。

值对象:可以独立存在,但是无法进行自我管理,可以描述任何聚合根 / 实体,无生命周期的概念,也可以理解为永生(无限生命周期)。

把任何一个复杂的事物化繁为简的方式就是不断的提炼,归约。动静分离就是归约的一种方式,笔者我认为在 DDD 中 “动” 就是聚合根和实体,“静”就是值对象,如果能不断的提炼出 “静” 的部分对于整个领域的理解复杂度是有帮助的。“动”是复杂的 “似生命体”,是有寿命的;“静” 是简单的“死物”,它一直存在,而且一直在那里。

因此笔者我认为只要是可独立存在的对象都可以使用 Repository 来持久化。那么我们的 Demo 中,既然已经决定将等级和折扣率建立为值对象的话,接下去的持久化要怎么做呢?请看 Part Ⅳ。

三、场景 2 的思考

场景 2 里有一个比较容易踩进去的坑,为了持久化把原本设计成值对象的改为实体(特别是针对一个值对象的集合的时候,需要一个唯一表示来区分其中多个值对象)。

特别特别注意,当在脑海中出现这个意识的时候,需要在思维上保证从领域建模的角度思考,而不是为了持久化。

因为实体建模是一种数据化的建模方式,很大程度上收到了数据库范式的影响。这里引用[Vaughn Vernon]《实现领域驱动设计》中的 4 个问题:

  1. 我们当前建模的概念表示领域中的一个东西呢,还是只是用于描述和度量其它东西?

  2. 如果该概念起描述作用,那么它是否满足以下几大特征?

①它度量或者描述了领域中的一件东西。
    ②它可以作为不变量。
    ③它将不同的相关的属性组合成一个概念整体。
    ④当度量和描述改变时,可以用另一个值对象予以替换。
    ⑤它可以和其他值对象进行相等性比较。
    ⑥它不会对协作对象造成副作用。

  1. 将该概念建模成实体是不是只是持久化机制上的考虑?

  2. 将该概念建模成实体是不是因为它拥有唯一标识,我们关注的是对象实例的个体性,并且需要在其整个生命周期中跟踪其变化?

如果你的答案是 “描述,是,是,不是”,那么此时你应该坚持用值对象。我们不应该让持久化影响到领域对象的建模。

那么我们该怎么做呢?请看 Part Ⅳ。

四、避坑方式

数据库选型上在 Nosql 出现后,可以很好的避免了这里的持久化问题。但是弱化的关系之后导致一些查询问题需要做更多的工作去解决。并且我认为在业务复杂的电商系统中,用关系型数据库作为最终的技术选型还是最常见的一种方式。那么在使用关系型数据库的情况下,我们可以通过使用以下几种方式解决这个问题:

  1. 把值对象中的属性作为所属实体 / 聚合根的数据列来存储。

缺点:会导致数据表列数较多,在一个数据页存储的数据量变少,影响数据库表的使用性能。

  1. 把整个值对象序列化后作为所属实体 / 聚合根的数据列来存储。

缺点:出现大数据长度的列,页会导致在一个数据页存储的数据量变少,影响数据库表的使用性能。另外无法直接通过 SQL 来查询值对象的属性,需要自定义做反序列化操作。

  1. 如果是 ORM 类的框架,那么建立一个基类通过 protected 的标识列来委派这个唯一标识,但是在实际运用过程中是感觉不到这个存在的。

  2. 如果不是 ORM 框架或者本身框架支持,那么可以通过无主键的方式存入到数据表中。

缺点:是无法嵌套模型。

五、实践

我想上面说的 4 种方式中的 1、2、4 都比较好理解,所以在我们的 Demo 中,我准备使用第 3 种方式来处理当前的值对象持久化。先看下我们当前抽象出来的几个核心类。

  1. public abstract class ValueObject
  2. {
  3. }
  4. public abstract class Entity
  5. {
  6. }
  7. public abstract class Aggregate : Entity
  8. {
  9. }

简单的不能再简单的,仅仅起到了一个表示作用,但是也体现出了这 3 个对象之间的联系。

根据本篇讲述的内容,再进行一次进行抽象。变成如下图 1 所示的一个关键模型。

9 小心陷入值对象持久化的坑 - 图1

图 1

其中需要注意的是 Entity 中的 ID 其实就是对 DelegateIdentifier.Identity 的引用,也就是仅仅为了做一个 Public 的公开。

另外 AloneStorableValueObject 与 ValueObject 唯一不同是其需要持久化并独占一个数据表,而 ValueObject 是不需要持久化或者跟着所属的聚合根持久化的。然后我们的 IRepository变成下面这样约束:

  1. public interface IRepository<T> where T : DelegateIdentifier, IAloneStorable
  2. {
  3. string NextIdentity();
  4. void Save(T aggregate);
  5. T GetByIdentity(string identity);
  6. }

这样只有继承了 AggregateRoot 和 AloneStorableValueObject 可以有自己的 Repository 了。

六、结语

从业务角度来说设计就是不断的梳理业务再抽象建模的过程,不是一蹴而就,也没有一招打遍天下的方式。需要不断的演进、找到最适合的设计方式。

从更泛角度来说设计也是约束、定义规则的过程,一套清晰的规则可以为整个项目的所有开发者往共同的目标前进起到事半功倍的效果。

本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo9


原创文章,转载请注明本文链接: https://zacharyfan.com/archives/174.html

关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描二维码~

9 小心陷入值对象持久化的坑 - 图2

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的 “仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。
https://zacharyfan.com/archives/174.html