一、限界上下文
GEEK TALK
1.1 前言
1.2 子域和限界上下文
1.3 划分领域(限界上下文)的依据
通用语言:在做领域划分之前一定要统一领域通用语言,如果一个名词在用语言描述的时候在不同语境有不同含义,那么就应该在不同语境中创建不同上下文。比如book,在写作阶段就是草稿,在出版阶段就是一个出版物,在购买阶段是一个书籍类商品,在发货阶段是一个物流订单。那么就应该按照书的角色进行归类,区分出上下文。正所谓,在商言商,在领域就应该说领域的通用语言。
领域职责:不同领域想要达成的目标是不一样的,每个领域都有自己最终要完成的事情,即通过领域知识,完成领域活动,最后完成领域职责。
领域角色:不同领域的角色也不尽相同,前端领域里可能需要ue角色、fe角色。后端领域里可能需要java研发、dba等角色。同时上边举的book的例子,book在不同领域的角色也是不一样的。再通俗一点,你在学校是学生角色,上班是员工角色。
领域知识:不同领域包括的知识是不一样的,比如后端和前端,后端可能需要了解服务器相关的知识、前端需要了解的是界面相关的知识。
领域活动:不同领域的职责也是不同的,在领域内进行的活动也不一样,比如前端需要构建前端页面活动。后端处理数据库交互活动。领域活动会利用到领域知识,如果进行领域活动的时候却不具备这个领域的知识,那么说明领域划分是不合理的。
领域关注点:不同领域关注点不同,拿person举例,person有身份证信息、年龄、身高、体重、工作、专业等信息,但是在不同领域对person的关注点是不一样的,银行办信用卡不需要身高体重信息、参加奥运会却关注身高体重,相亲时不会关注身份证信息,但却关注你的工作、年龄等。
1.4 落地经验
运营人员:运营人员要配置模型的各种规则,但是频次相对较低。
用户:用户会使用运营人员配置的规则,使用频次较高。
1.5 小结
领域就是有一个范围,在这个范围内有不同的角色,每个角色都有该角色应该具备的领域知识,各角色之间通过自己掌握的知识完成彼此协作,完成一些领域活动,产生一些领域事件,最终完成领域职责。
划分领域的依据就是领域职责(目标)、领域关注点、完成职责需要的角色、角色需要的知识、角色需要执行的活动。
事件风暴的过程也是识别领域活动、领域职责、领域角色、领域事件、领域知识的过程。
二、实体
GEEK TALK
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 属性。
补充:
值对象也是实体对象的属性之一,它没有身份标识,也不可改变。比如上面的签到,学生在今天签到之后,创建的签到记录,就是学生的值对象,这条记录创建了,就不可改变了(排除黑入教务系统篡改个人数据的情况)。值对象更多的信息,会在后面提到。
三、值对象
GEEK TALK
3.1 前言
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
可以展现领域概念;学生实体的年龄,string与Name、int与Age相比,显然后者更加直观得体现了业务含义。可以封装显而易见的领域概念;比如对于一个经销商4s店店位置经度和纬度都是这个4s店实体实体店属性,但是合成一个坐标值对象更能展示实体店领域概念。更好的封装利于自我领域行为的验证能力。保证每次生成得值对象都是正确的。
业务对它相等的判断是根据值还是身份标识。前者是值对象,后者是实体。当我们从图书馆判断一本书是否相同,即使名字相同也并非同一本书,在系统中,只有id相同才是同一本书;但我们判断一个位置,当经纬度相同的时候就是同一个位置。这个时候图书就是定义为实体,坐标定义为值对象。
确定对象的属性值是否会发生变化,如果变化了,究竟是产生一个完全不同的对象,还是维持相同的身份标识。在员工的出勤记录业务场景中,依据相等性进行判断时,可以任务出勤记录值相等的就是同一条记录,但如果员工提出补卡,对记录状态修改对时候,其同一性就只能通过唯一的身份标识进行判断,这意味这应该被定义为实体。
生命周期是手动的。值对象没有身份标识,意味着无需管理其生命周期。但是实体无需关注。
END