如何一步一步用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所示的一个关键模型。

【图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设计一个电商网站(九)—— 小心陷入值对象持久化的坑的更多相关文章
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
- 如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
阅读目录 前言 怎么卖 领域服务的使用 回到现实 结语 一.前言 上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计.现在把剩余的“卖”这个动作给做了.这里提醒一下,正常情况下,我们的每一步业 ...
- 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域
一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...
- 如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
阅读目录 前言 准备 实现 结语 一.前言 最近实在太忙,上周停更了一周.按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理.从整个流程来看,这里需要用户填写的信息是最多的,那么 ...
- 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...
随机推荐
- Vue.js 2.0 和 React、Augular等其他框架的全方位对比
引言 这个页面无疑是最难编写的,但也是非常重要的.或许你遇到了一些问题并且先前用其他的框架解决了.来这里的目的是看看Vue是否有更好的解决方案.那么你就来对了. 客观来说,作为核心团队成员,显然我们会 ...
- 使用python抓取婚恋网用户数据并用决策树生成自己择偶观
最近在看<机器学习实战>的时候萌生了一个想法,自己去网上爬一些数据按照书上的方法处理一下,不仅可以加深自己对书本的理解,顺便还可以在github拉拉人气.刚好在看决策树这一章,书里面的理论 ...
- PHP-----文件系统的交互
本文讲解php中于文件交互中所使用的函数 代码示例 <html> <head> <title> File Detail </title> </he ...
- Redis简单案例(二) 网站最近的访问用户
我们有时会在网站中看到最后的访问用户.最近的活跃用户等等诸如此类的一些信息.本文就以最后的访问用户为例, 用Redis来实现这个小功能.在这之前,我们可以先简单了解一下在oracle.sqlserve ...
- Stack Overflow 排错翻译 - Closing AlertDialog.Builder in Android -Android环境中关闭AlertDialog.Builder
Stack Overflow 排错翻译 - Closing AlertDialog.Builder in Android -Android环境中关闭AlertDialog.Builder 转自:ht ...
- Oracle 分页
--1:无ORDER BY排序的写法.(效率最高) --(经过测试,此方法成本最低,只嵌套一层,速度最快!即使查询的数据量再大,也几乎不受影响,速度依然!) SELECT * FROM (SELECT ...
- Linux测试环境搭建的学习建议
随着Linux应用的扩展许多朋友开始接触Linux,根据学习Windwos的经验往往有一些茫然的感觉:不知从何处开始学起.这里介绍学习Linux测试环境搭建的一些建议. 一.Linux测试环境搭建从基 ...
- 了解 ARDUINO 101* 平台
原文链接 简介 作为一名物联网 (IoT) 开发人员,您需要根据项目的不同需求,选择最适合的平台来构建应用. 了解不同平台的功能至关重要. 本文第一部分比较了 Arduino 101 平台和 Ardu ...
- Xamarin.Android之Fragment Walkthrough
利用Fragment设计能够兼容不同屏幕的应用 这里我们先围观下最后的成果图,给读者打打气: 普通手机上显示的结果: 在平板上显示的结果: 笔者要郑重声明下,虽然看似是两种不同的显示效果,但是同一个应 ...
- .NET 环境中使用RabbitMQ
在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统时,这种分布式的调用与通信变得越发重要.其次,系统中一般会有很多对实时性要求不高的但是执行起来比较较耗时的地方,比如发送短信 ...