如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

时间:2021-07-12 21:41:27

本系列所有文章

如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

如何一步一步用DDD设计一个电商网站(二)—— 项目架构

如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域

如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户

如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发

如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备

如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展

阅读目录

一、前言

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

    public interface IRoleDiscountRelationRepository// :    IRepository<RoleDiscountRelation>
{
RoleDiscountRelation Get(string roleId);
}

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

    public interface IRepository<T> where T : AggregateRoot
{
string NextIdentity(); void Save(T aggregate); T GetByIdentity(string identity);
}

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

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

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

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

二、场景1的思考

  整个问题的解决方式,首先需要梳理清楚3个基本概念:“聚合根”、“实体”、“值对象”这3者的关系。这个我在(如何一步一步用DDD设计一个电商网站(二)—— 项目架构)中有提及。因为涉及到持久化,所以我们可以再通过分析这3种对象的生命周期来帮助思考。

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

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

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

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

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

三、场景2的思考

  场景2里有一个比较容易踩进去的坑,为了持久化把原本设计成值对象的改为实体(特别是针对一个值对象的集合的时候,需要一个唯一表示来区分其中多个值对象)。特别特别注意,当在脑海中出现这个意识的时候,需要在思维上保证从领域建模的角度思考,而不是为了持久化。因为实体建模是一种数据化的建模方式,很大程度上收到了数据库范式的影响。这里引用[Vaughn Vernon]《实现领域驱动设计》中的4个问题:

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

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

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

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

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

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

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

四、避坑方式

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

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

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

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

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

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

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

    缺点:是无法嵌套模型。

五、实践

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

    public abstract class ValueObject
{
}
    public abstract class Entity
{
}
    public abstract class Aggregate : Entity
{
}

  简单的不能再简单的,仅仅起到了一个表示作用,但是也体现出了这3个对象之间的联系。根据本篇讲述的内容,再进行一次进行抽象。变成如下图1所示的一个关键模型。

如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

                            【图1】

  其中需要注意的是Entity中的ID其实就是对DelegateIdentifier.Identity的引用,也就是仅仅为了做一个Public的公开。另外AloneStorableValueObject与ValueObject唯一不同是其需要持久化并独占一个数据表,而ValueObject是不需要持久化或者跟着所属的聚合根持久化的。然后我们的IRepository<T>变成下面这样约束:

    public interface IRepository<T> where T : DelegateIdentifier, IAloneStorable
{
string NextIdentity(); void Save(T aggregate); T GetByIdentity(string identity);
}

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

六、结语

  从业务角度来说设计就是不断的梳理业务再抽象建模的过程,不是一蹴而就,也没有一招打遍天下的方式。需要不断的演进、找到最适合的设计方式。从更泛角度来说设计也是约束、定义规则的过程,一套清晰的规则可以为整个项目的所有开发者往共同的目标前进起到事半功倍的效果。

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

作者:Zachary
出处:https://zacharyfan.com/archives/174.html

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

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

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

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

如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑的更多相关文章

  1. 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

    阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...

  2. 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

     阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...

  3. 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

    阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...

  4. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

  5. 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发

    阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...

  6. 如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户

    阅读目录 前言 怎么卖 领域服务的使用 回到现实 结语 一.前言 上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计.现在把剩余的“卖”这个动作给做了.这里提醒一下,正常情况下,我们的每一步业 ...

  7. 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域

    一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...

  8. 如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备

     阅读目录 前言 准备 实现 结语 一.前言 最近实在太忙,上周停更了一周.按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理.从整个流程来看,这里需要用户填写的信息是最多的,那么 ...

  9. 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

    阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...

随机推荐

  1. Oracle&lowbar;SQL函数-分组函数

    分组函数 什么是分组函数 分组函数作用于一组数据,并对一组数据返回一个值 组函数类型:主要有6种 AVG - 平均 COUNT - 计数 MAX - 最大 MIN - 最小 SUM - 求和 STDD ...

  2. SQLite常用命令总结

    1) 创建数据库文件: >SQLite3 d:\test.db 回车 就生成了一个test.db在d盘. 这样同时也SQLite3挂上了这个test.db 2) 用.help可以看看有什么命令 ...

  3. JavaWeb学习记录(二十七)——定时发送邮件ServletContextListener监听实现

    public class EmailSendListener implements ServletContextListener{ @Override    public void contextDe ...

  4. Codeforces Round &num;259 &lpar;Div&period; 2&rpar;-D&period; Little Pony and Harmony Chest

    题目范围给的很小,所以有状压的方向. 我们是构造出一个数列,且数列中每两个数的最大公约数为1; 给的A[I]<=30,这是一个突破点. 可以发现B[I]中的数不会很大,要不然就不满足,所以B[I ...

  5. MVC4 学习笔记01

    1 . ASP.NET MVC 中 ActionResult 和 ViewResult 在使用上的区别是什么?要注意什么吗? ActionResult 是一个抽象(abstract)类,ViewRes ...

  6. linux 查看进程 和 杀死进程

    ps ax 显示当前系统进程的列表 PID TTY      STAT   TIME COMMAND ps aux 显示当前系统进程详细列表以及进程用户 USER       PID %CPU %ME ...

  7. 芝麻HTTP:Python爬虫入门之正则表达式

    1.了解正则表达式 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符.及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来 ...

  8. Deeplearning&period;ai课程笔记--汇总

    从接触机器学习就了解到Andrew Ng的机器学习课程,后来发现又出来深度学习课程,就开始在网易云课堂上学习deeplearning.ai的课程,Andrew 的课真是的把深入浅出.当然学习这些课程还 ...

  9. day19&colon;常用模块&lpar;collections&comma;time&comma;random&comma;os&comma;sys&rpar;

    1,正则复习,re.S,这个在用的最多,re.M多行模式,这个主要改变^和$的行为,每一行都是新串开头,每个回车都是结尾.re.L 在Windows和linux里面对一些特殊字符有不一样的识别,re. ...

  10. linux dns

    linux 用户相关的 root   相当于QQ群主 sudo  QQ群管理员 普通用户  QQ群水军 root  UID 是 0   组UID也是0  普通用户UID从1000开始 查看用户id 信 ...