数据架构设计就是以数据为核心,来梳理整个业务处理流程。
数据架构设计环节首先要进行领域模型的设计,然后将领域模型的设计转换成数据库设计和程序设计。
3.1 数据架构的设计过程
早期的数据架构设计是以数据库设计为核心的设计过程;先理需求,再设计数据库,后开发;当系统规模小、团队人员小时,这种方式能有效推动项目的上线,不会存在较大的问题;但是当系统规模大、团队成员多时,会给整个项目带来风险。
推荐采用面向对象的设计过程来分析设计系统(OOA/OOD)。拿到需求后,先进行用例模型的设计,明确系统要实现哪些功能;接着进行领域模型的设计,分析系统的业务实体;在领域模型的设计过程中,采用类图的形式,确认每个类的属性及方法(类的属性表述数据结构,方法描述对数据结构的处理);然后,将领域对象按照某种对应关系转换成数据库的设计,实现领域对象的持久化。
数据架构设计环节,首先进行领域模型的设计,然后将领域模型的设计转换成数据库设计以及程序设计。
3.2 基于领域的数据库设计
1、如何将领域模型转换成数据库设计?
答:领域模型中的一个类可以转换成数据库中的一个表,类中的属性可以转换成表中的字段,但这里的关键是如何处理类与类之间的关系,如何将其转换成表与表之间的关系。有五种类型的关系需要转换:
(1)一对一关系
一对一的关系通过将一个实体的主键作为另一个实体的外键实现,每个实体对应一张数据库表。
(2)多对一关系
多对一的关系通过在多的一方添加一的一方的外键来实现。在查询多的一方时,先分页,然后再补充当前页的关联对象。例:学生 N – 1 老师;在学生表中添加老师的外键,查询学生时,先查询、分页,再对结果补充老师的内容。
(3)一对多关系
一对多关系往往表达的是一种主 – 子表的关系(例:订单 – 订单详情)。一对多关系在数据加设计上比较简单,就是在子表中增加一个外键去引用主表中的主键。除此之外,在程序 的值对象设计时,主对象中也应当有一个集合的属性变量去引用子对象。
(4)多对多关系
多对多关系,比较典型的案例就是角色 – 权限。这种多对多关系在对象设计时,可以通过一个中间表“功能 – 角色关联表”,该表的主键就是关系双方的主键进行组合所形成的联系主键。
(5)继承关系
1)单表
如果继承关系的子类不多,且每个子类的个性化字段也不多,则可以使用一个表来记录整个继承关系。在这个表的中间有一个标识字段,标识表中的每条记录到底是哪个子类。优点:简单;缺点:造成“表稀疏”
2)每个子类一张表
如果每个子类都有很多的个性化子段,可以每个子类一个表,这些表共用一个主键,即这几个表的主键生成器是一个,某个主键值只能存在于某一个表中,不能存在于多个表中。
如果业务需求是在前端查询时每次只能查询某一个子类的信息,采用这种方案即可。
3)父类一张表,各子类分别对应各自的表
如果业务需求是查询所有的信息,则用这种方案合适。
3.3 基于领域的程序设计
数据架构阶段要完成的另一项任务,就是基于业务需求中的功能性需求进行业务梳理,完成相应的程序设计。
在该阶段进行的程序设计实际上最顶层的程序设计。也就是说,从业务上去梳理每个模块的每个功能中最主要的都有哪些服务、哪些实体,以及这些服务与实体主要包含哪些行为、哪些方法、执行什么流程。
1、如何让领域模型指导程序设计?
答:采用领域驱动设计DDD来指导程序设计。
3.3.1 服务、实体与值对象
领域模型映射到程序设计,最终都会落实到三种类型的对象设计:服务、实体与值对象。
服务:
服务标识的是那些在领域对象之外的操作与行为。在领域驱动设计中,服务通常承担了两类职责:接收用户的请求、执行某些操作。
实体:
实体就是那些通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象。
值对象:
值对象代表的是真实世界中那些一成不变的、本质性的事物。
可变性是实体的特点,而不变性是值对象的特点。
3.3.2 贫血模型与充血模型
将业务领域模型转换为程序设计,有两种设计思路:贫血模型与充血模型。
贫血模型:
在软件设计中有很多的POJO(Plain Ordinary Java Object)对象,它们除了有一堆的get/set方法外,几乎没有任务业务逻辑,这样的设计被称为“贫血模型”。
充血模型:
就是将领域对象的原貌直接转换为实体对象的设计。
贫血模型的业务逻辑在Service中实现;充血模型的业务逻辑在领域对象中实现。
充血模型的优点:
充血模型保持了领域模型的原貌,领域模型可直接转换成程序的设计。当领域模型随着业务变更而频繁甚至大幅度调整时,可以比较直接地映射成程序的变更,代码修改起来比较直接。此外,充血模型保持了对象的封闭性,使得领域模型在面临多态、继承等复杂结构时易于变更。(充血模型在理论上非常优雅,但在工程实践上却不尽如人意。)
贫血模型的优点:
1)贫血模型比充血模型更加简单易行
在贫血模型中,MVC层直接调用Service,Service通过DAO进行数据访问。在这个过程中,每个DAO都只查询数据库中的某个表,然后直接交给Service去使用,去完成各种处理。
2)充血模型需要有更强的设计与协作能力
3)贫血模型更容易应对复杂的业务处理场景
1、哪些业务逻辑需要封装起来按照充血模型设计呢?(作者的经验)
答:
1)如果在领域模型中出现了类似继承、多态的情况,则应当将继承与多态的部分以充血模型的形式在领域对象中实现
2)如果软件设计的过程中需要对一些类型或者编码进行转换,则将转换的部分封闭在领域对象中
3)希望在软件设计中能更好地表现领域对象之间的关系,这时,除了要将领域模型中的关系体现在领域对象的设计上之外,还需要有仓库与工厂的支持。
4)在“聚合”情况下按照充血模型进行形式会更合适
3.3.3 聚合
聚合表达的是真实世界中那些整体与部分的关系。所谓整体与部分的关系,就是当整体不存在时,部分就没有了意义。部分是整体的一个部分,与整体有相同的生命周期。
有了聚合关系,部分就被封装在整体里面,这时候就形成了一种约束,即外部程序不能跳过整体去操作部分,对部分的操作都必须要通过整体。这时,整体就成为了外部访问的唯一入口,被称为“聚合根”。
3.3.4 仓库与工厂
1、仓库(仓储):
仓储表示聚合的集合。
仓储所表现出来的集合外观,仅仅是一种模拟,除了测试以外,没有理由使用内存中真正的集合来创建仓储。
不应该为所有实体建立仓储,只有聚合才拥有仓储。
仓储用来重建已持久化的聚合,而工厂用于新建聚合。
2、仓储与数据访问层DAO 的区别
仓储限定了只能通过聚合根来持久化和检索领域对象,以确保所有改动和不变性由聚合处理。
仓储通过隐藏聚合持久化和检索的底层技术实现领域层的持久化无关性(即领域层不需要知道如何持久化领域对象)。
仓储在数据模型和领域模型定义了一个边界。
DDD中的工厂与设计模式中的工厂不是同一个概念。DDD中的工厂的主要工作是通过装配来创建领域对象,是领域对象生命周期的起点。
例:系统要通过ID装载一个订单,这时订单仓库会将这个任务交给订单工厂。订单工厂就会分别调用订单DAO、订单明细DAO和用户DAO去查询,然后将得到的订单对象、订单明细对象、用户对象进行装配,即将订单明细对象与用户对象分别设置到订单对象的“订单明细”与“用户”属性中。最后,订单工厂将装配好的订单对象返回给订单仓库。
3.3.5 问题域和限界上下文
子域:将整个系统划分成许多相对独立的业务场景,在一个一个的业务场景中进行领域分析与建模,我们称这样的业务场景为“问题子域”,简称“子域”。
问题域:真实世界的业务与问题叫做“问题域”,这里面的业务规划与知识叫“业务领域知识”。
CB限界上下文:对一个复杂系统的领域驱动设计,就是以子域为中心进行领域建模,绘制出一张一张的领域模型,然后以此为基础指导程序设计。这一张一张的领域模型,称为“限界上下文”(Context Bound,CB)。
DDD中限界上下文的设计,很好地体现了高质量软件设计中“单一职责原则”的要求,即每个限界上下文中实现的业务都是因同一个原因而变更的软件的功能。
“限界上下文内的高内聚”就是每个限界上下文内实现的功能都是因同一个原因而变更软件的代码,因为这个原因的变化才需要修改这个限界上下文,除此之外的修改与它无关。
“限界上下文耦合”就是限界上下文通过上下文地图相互调用时是通过接口进行调用。
逻辑过界:通过限界上下文将整个系统按照逻辑进行划分,但从物理上它们都还是一个项目,运行在一个JVM中。这种限界上下文只是“逻辑边界”。
物理边界:将单体应用转型成微服务架构以后,各个限界上下文都是运行在各自不同的微服务中,是不同的项目、不同的JVM。不仅如此,在进行微服务拆分的同时,数据库也进行了拆分,每个微服务使用不同的数据库。这样,当各个微服务要访问用户信息时,它们没有访问用户数据库的权限,就只能通过远程接口去调用“用户”微服务开放的相关接口。这时,这种限办上下文就真正变成了“物理边界”。



