导读
畛域驱动设计 (DDD) 最简洁的形容可能是:如何在明确的限界上下文中创立通用语言的模型。通过 DDD 思维设计开发的软件,在领域专家、开发者和软件自身之间不存在“翻译”,三者通过在限界上下文下的通用语言间接示意。而这个系列则是咱们团队对 DDD 模式的摸索和落地,旨在能帮忙大家逐渐揭开 DDD 的神秘面纱。
全文 5259 字,预计浏览工夫 14 分钟。
一、限界上下文
1.1 前言
DDD 分为策略设计和战术设计,策略设计就是划分子域和限界上下文的过程。畛域划分为子域的通用划分模式是把畛域划分为 外围子域、撑持子域、通用子域。咱们在落地过程中经常会很容易划分出外围子域,个别设计 mvp 的时候 mvp 就是外围子域。然而畛域划分出外围子域、撑持子域和通用子域之后就算划分实现了吗?
1.2 子域和限界上下文
实际上子域也是畛域,一个公司不同部门关注的是一个大畛域的不同子畛域,在你关注的畛域内也须要做这种子域的划分。
比方百度这个大公司,有很多部门,这些部门都属于互联网畛域,然而每个部门又有本人关注的畛域,比方游戏部门关注的是游戏畛域、搜寻部门关注的是搜寻畛域。
不同部门的畛域还能够再持续划分出本人关注的畛域的外围域和撑持子域,所以整体上,畛域的划分就像一棵树。咱们回到本人关注的畛域,基于这个畛域做划分。咱们会把这个关注的畛域划分为外围域、撑持域和通用域,个别每个域都由一个小团队负责(康威定律)。
如果一个团队的工作是撑持域,那么这个撑持域就是他们的外围域,他们能够对此再做粗疏的划分,何时划分到头呢?用一个具体的限界上下文解决这个叶子畛域的所有问题,并且畛域通用语言在这个上下文中没有二义性,那么就算划分到头了。
划分到树叶的畛域都是待解决的问题,也叫问题域,而限界上下文呢就是用来解决这个域内所有问题的模型。
针对限界上下文与畛域的对应关系 Vernon 给出了倡议,最好是 1:1 的关系,当然也有其余说法如 1:N,N:N,自己认同 Vernon 的说法,如果子域对应多个限界上下文,那么只能说该子域还能够再划分为子子域,由子子域去对应每个限界上下文。划分好子域和限界上下文后,限界上下文的主题就是解决这个子畛域的问题,伎俩就是 DDD 战术建模,工具就是畛域通用语言,限度就是畛域通用语言不能有二义性。
1.3 划分畛域(限界上下文)的根据
- 通用语言:在做畛域划分之前肯定要对立畛域通用语言,如果一个名词在用语言形容的时候在不同语境有不同含意,那么就应该在不同语境中创立不同上下文。比方 book,在写作阶段就是草稿,在出版阶段就是一个出版物,在购买阶段是一个书籍类商品,在发货阶段是一个物流订单。那么就应该依照书的角色进行归类,辨别出上下文。正所谓,在商言商,在畛域就应该说畛域的通用语言。
- 畛域职责:不同畛域想要达成的指标是不一样的,每个畛域都有本人最终要实现的事件,即通过畛域常识,实现畛域流动,最初实现畛域职责。
- 畛域角色:不同畛域的角色也不尽相同,前端畛域里可能须要 ue 角色、fe 角色。后端畛域里可能须要 java 研发、dba 等角色。同时上边举的 book 的例子,book 在不同畛域的角色也是不一样的。再艰深一点,你在学校是学生角色,下班是员工角色。
- 畛域常识:不同畛域包含的常识是不一样的,比方后端和前端,后端可能须要理解服务器相干的常识、前端须要理解的是界面相干的常识。
- 畛域流动:不同畛域的职责也是不同的,在畛域内进行的流动也不一样,比方前端须要构建前端页面流动。后端解决数据库交互流动。畛域流动会利用到畛域常识,如果进行畛域流动的时候却不具备这个畛域的常识,那么阐明畛域划分是不合理的。
- 畛域关注点:不同畛域关注点不同,拿 person 举例,person 有身份证信息、年龄、身高、体重、工作、业余等信息,然而在不同畛域对 person 的关注点是不一样的,银行办信用卡不须要身高体重信息、加入奥运会却关注身高体重,相亲时不会关注身份证信息,但却关注你的工作、年龄等。
1.4 落地教训
在落地过程中咱们遇到了一个建模问题:
咱们的服务有两个角色应用:
- 经营人员:经营人员要配置模型的各种规定,然而频次绝对较低。
- 用户:用户会应用经营人员配置的规定,应用频次较高。
在我的项目初期因为设计问题,最终放弃了拆分这两个上下文,而是应用雷同的上下文进行了建模。
这个问题的实质是咱们没有想好畛域划分,当初回头想想,咱们解决的是一个外围域,然而这个外围域又能够分为两个子域:一个是配置平台子域,一个是用户应用子域。
两个子域的关注点是不同的,并且变动频次也不同,后续用户应用上下文会做横向扩大,咱们目前的单体架构尽管能做扩大,然而不合乎繁多职责准则,因为用户应用平台集成了配置性能,而配置性能是不应该随着用户性能进行扩大的。在拆分过程中,会有很多代码是重叠的,咱们的服务中就有很多 Aggregate 聚合,在两个上下文中有很多字段是一样的,但反复并不一定是谬误,因为反复的代码关注点和变动频率是不一样的。这里咱们介绍了利用角色进行关注点辨别,进而划分子域和限界上下文的办法,实际上也能够依据其余条件对畛域进行划分,划分只有保障概念绝对独立,关注点绝对独立,划分后没有失落问题就能够。
1.5 小结
- 畛域就是有一个范畴,在这个范畴内有不同的角色,每个角色都有该角色应该具备的畛域常识,各角色之间通过本人把握的常识实现彼此合作,实现一些畛域流动,产生一些畛域事件,最终实现畛域职责。
- 划分畛域的根据就是畛域职责(指标)、畛域关注点、实现职责须要的角色、角色须要的常识、角色须要执行的流动。
- 事件风暴的过程也是辨认畛域流动、畛域职责、畛域角色、畛域事件、畛域常识的过程。
二、实体
2.1 前言
实体是畛域驱动设计中十分重要的一个局部,Len Silerston 说:“实体是一个重要的概念,企业心愿建设和存储的信息都是对于实体的信息”。在 DDD 中,实体的构建是重中之重。
2.2 什么是实体
实体,是谓词形容的主体。它蕴含了其余领域,如引起属性变动和状态迁徙的动作。一个典型的实体应该具备 3 个因素:
-
身份标识:
- 通用类型:ID 值没有业务含意,惟一即可。
- 畛域类型:通常与各个界线上下文的实体对象无关。
-
属性:阐明主体的动态特色,并持有数据与状态。能够划分为原子属性和组合属性。划分的根据是:该属性是否存在束缚规定、组合因子或属于本人的畛域行为。
-
原子属性:
- name
-
组合属性:
- price(num, unit);
- 组合属性是一种很好的特质,当一个实体有了一些组合属性后,一些细小的概念 对应的职责 (根本校验、计算) 将由各自的属性进行负责,而实体更关注本身概念。
-
-
畛域行为:
- 变更状态的畛域行为:实体对象是容许调用者扭转状态的,这样就产生了变更状态的畛域行为(办法名上不倡议用 set/get, 而是更具备业务含意的办法名,这样更具备畛域逻辑(增强))
- 自力更生的畛域行为:对象只操作了本人的属性,而不依赖于内部属性。(如校验一份外卖的总金额、总数量 与外卖中各个单品的关系)
- 互为合作的畛域行为:须要调用者提供必要的信息(个别通过办法参数传入),这样就造成了畛域对象之间互为合作的畛域行为。
增删改查。
2.3 构建实体的根据
在 DDD 设计中,咱们将开发者的眼帘从数据库移到了实体上,以往咱们在设计一个零碎时,会关注要建设多少张表,而咱们在 DDD 中,则须要关注如何建设实体,这两者的异同点在于:
- 雷同:在 mvc 的开发模式中,开发者通过浏览 dao 层的表构造,就能理解到整个零碎大抵的架构与作用。同样的,在 DDD 中开发者通过浏览实体的属性,就能理解到整个零碎大抵的架构与作用。
- 不同:在 DDD 中,一个实体的属性可能只由一张表组成,也可能由多张表组成,也可能是由 mysql 和 redis 独特组成,在实体所在的畛域中,咱们并不关怀它的底层(数据层)是如何实现的,咱们只关怀这个实体。
举个例子
对于一个学生信息管理系统而言,咱们设计了一个学生的实体。
type Student struct {
ID uint64
Name string
Sex string
Class string
IsLate int
Sign *Sign
}
type Sign struct {SignTime time.Time}
func (stu *Student) StudentSign() {isLate := TimeCheck()
stu.IsLate = isLate
// flush redis...
}
以上实体的构造能够简略概括为:
- 身份标识:ID
- 属性:Name、Sex、Class、IsLate、值对象(Sign)
- 畛域行为:Sign()
在咱们的数据库设计中,Student 的根底信息,可能只包含了 ID、Name、Sex、Class 这四个字段,那 IsLate 字段呢?咱们将学生 IsLate 属性写进缓存里,不便某些监察管理系统的高频查问,同时咱们通过 Sign()办法进行学生签到状态的变更,咱们在 Sign 办法中进行校验后,批改这个学生实体的 IsLate 属性。
补充:
值对象也是实体对象的属性之一,它没有身份标识,也不可扭转。比方下面的签到,学生在明天签到之后,创立的签到记录,就是学生的值对象,这条记录创立了,就不可扭转了(排除黑入教务零碎篡改集体数据的状况)。值对象更多的信息,会在前面提到。
三、值对象
3.1 前言
值对象是实体的一个重要组成部分,如何正确应用值对象,也是 DDD 畛域驱动设计的一个难题。本文将介绍值对象的概念与应用办法。
3.2 概念
值对象是实体对象的属性,通常代表重量、性质、关系、场合、工夫或地位 / 姿势。当实体属性须要体现出其属性的意义,并为这个意义提供相干性能,能够设置为值对象。比方一家公司所在的省 / 市 / 区 / 街道能够合成值对象示意这家公司的地址属性。
3.3 特点
- 值对象不可变。建模优先思考值对象,因为值对象没有身份示意的累赘,自身不可变。值对象自身最多是不可变性。
- 值对象领有的往往是“自力更生的畛域行为”。这些畛域行为可能让值对象的体现能力变得更加丰盛,更加智能。
3.4 畛域行为
那什么是值对象的畛域行为呢?
- 自我验证:值对象自我组合,能缩小实体类的验证。
- 自我组合: 值对象会波及对数据值的运算,为了加强值对象的运算能力,能够在外部进行数据组合。如 金额的单位换算。
- 自我运算: 依照业务规定对属性值运算的行为。如经纬度计算。
// NewCoordinateVo 初始化坐标值对象
func NewCoordinateVo(LongitudeStr string, LatitudeStr string) (*VoCoordinate, error) {
// 自我验证
Longitude, err := strconv.ParseFloat(LongitudeStr, 64)
if err != nil {return nil, fmt.Errorf("Longitude_input_err")
}
Latitude, err := strconv.ParseFloat(LatitudeStr, 64)
if err != nil {return nil, fmt.Errorf("Latitude_input_err")
}
return &VoCoordinate{
Longitude: Longitude,
Latitude: Latitude,
}, nil
}
3.5 F&Q
1、相比于一般属性,值对象有哪些劣势呢?
能够展示畛域概念;学生实体的年龄,string 与 Name、int 与 Age 相比,显然后者更加直观得体现了业务含意。能够封装不言而喻的畛域概念;比方对于一个经销商 4s 店店地位经度和纬度都是这个 4s 店实体实体店属性,然而合成一个坐标值对象更能展现实体店畛域概念。更好的封装利于自我畛域行为的验证能力。保障每次生成得值对象都是正确的。
2. 那么一个畛域的概念咱们用实体还是值对象呢?能够根据几点来判断?
业务对它相等的判断是依据值还是身份标识。前者是值对象,后者是实体。当咱们从图书馆判断一本书是否雷同,即便名字雷同也并非同一本书,在零碎中,只有 id 雷同才是同一本书;但咱们判断一个地位,当经纬度雷同的时候就是同一个地位。这个时候图书就是定义为实体,坐标定义为值对象。
确定对象的属性值是否会发生变化,如果变动了,到底是产生一个齐全不同的对象,还是维持雷同的身份标识。在员工的缺勤记录业务场景中,根据相等性进行判断时,能够工作缺勤记录值相等的就是同一条记录,但如果员工提出补卡,对记录状态批改对时候,其同一性就只能通过惟一的身份标识进行判断,这象征这应该被定义为实体。
生命周期是手动的。值对象没有身份标识,意味着无需治理其生命周期。然而实体无需关注。
多个判断条件是层层递进的,要确定一个畛域概念到底是实体还是值对象,须要审慎判断,综合考量。
—— END——
举荐浏览:
百度工程师带你玩转正则
Diffie-Hellman 密钥协商算法探索
贴吧低代码高性能规定引擎设计
浅谈权限零碎在多利熊业务利用
分布式系统要害门路提早剖析实际
百度工程师教你玩转设计模式(装璜器模式)