MVC有什么问题?什么是领域驱动设计?能解决什么?
传统的MVC会有什么问题
mvc结构
mvc结构的特点
- 对象只是数据的载体,它没有相关行为,可以理解为是对数据移动、处理和实现的过程,这种对象我们称之为贫血领域对象。
- 业务初期,我们的功能大都非常简单,普通的CRUD就能满足,此时系统是清晰的。随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越复杂。
mvc结构暴露的问题
- 所有的业务逻辑几乎集中在service层,包括什么缓存、mq、第三方的系统调用的操作,这会导致service层非常的臃肿,上下两层非常的薄,不同service之间的边界非常不清晰。
- 对一个model的操作(比如状态)可能会散落到很多个service中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。
- 所有依赖的外部组件都耦合到了service层,如果需要升级需要对service层进行大量改动,那么改动可能会影响业务的核心逻辑。
这和我们软件开发所追求的高内聚、低耦合、可维护性似乎点背道而驰。
领域驱动设计能解决的问题
30年以前,国外的软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans(国外大牛)将其定义为领域驱动设计(Domain-Driven Design,简称DDD)
DDD它是一个方法论,它指导软件开发人员怎样去划分领域(业务逻辑边界),领域之间的交互方法及依赖关系,定义了领域之中的元素类型以及领域驱动设计中的代码结构等等。
在这个方法论的指导下使得我们软件系统朝高内聚、低耦合、可维护性高的方向发展。
领域驱动设计几个重要的概念
先理解名词的含义,注意看加粗的字体,这对领域的设计很重要。
领域
现实世界中,领域包含了问题域和解决系统。一般认为软件是对现实世界的部分模拟。在DDD中,解系统可以映射为一个个限界上下文,限界上下文就是软件对于问题域的一个特定的、有限的解决方案。
限界上下文
一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。
领域服务
一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是指对象的范畴。
实体
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。mvc中的model其实是对实体的抽象,比如人(person)是一个抽象概念,黑人、白人是对抽象的进一步的实现。最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。
值对象
当一个对象用于对实体属性进行描述而没有唯一标识时,它被称作值对象(Value Object)。
例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的颜色信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。
聚合根
Aggregate(聚合)是一组相关实体的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。
Repository(仓库)
可以理解为实体的仓库,可以从数据库中拿出实体,也可以从缓存中拿出实体,这个概念的好处是,如果我的存储换了,我的缓存换了,只需要改变这一层即可,其他的业务逻辑几乎不用改动,这一层主要是操作数据库或者缓存。
防腐层
外部变化我们控制不了,但是内部变化是可控的,因为对于内部来说,我需要什么我自己是最清楚的。所以这一层的作用主要是用来对接外部系统,当外部系统发生变化的时候我只需要调整防腐层就好,内部的业务逻辑几乎不需要调整。
名词关系
说明:聚合根中包含实体和值对象,值对象也可以包含值对象,领域服务可以调用聚合根中的行为方法。也可以通过防腐层调用外部服务,多个领域服务组成限界上下文,多个限界上下文组成领域。
领域驱动设计的结构
DDD结构
- service-领域服务层
- repository-仓库层
- Aggregate Root-聚合根层
- entity-实体层
- value object-值对象层
- ACL-防腐层
service层可以理解为业务逻辑编排层,service层调用repository层获取聚合根对象,然后执行聚合根和实体相关的行为方法。也可以调用防腐层处理外部数据。
防腐层贯彻整个context,业务逻辑大部分封装到聚合根层和实体层,达到了逻辑内聚,实体行为复用的目的,多个上下文构成一个应用。
数据流转
领域驱动设计实践
举例
用户可以使用手机号码或者邮箱登录我们的系统,如果登录成功之后判断用户30天内是否是第一次登录,如果是就给用户发送一条短信,提示用户”欢迎回来“。(PM:怎么实现我不管,反正今天上线!)
分析
mvc实现代码结构
使用golang实现,java实现有需要的私聊我。
mvc结构实现
核心逻辑
controller层代码
比较简单,就不贴了。
model层代码
user.go
service层代码
user_service.go
问题一:对外部的依赖耦合严重
什么是外部的依赖?不属于当前域内的设施和服务都可以当成是外部依赖,比如数据库,数据库schema,RPC,消息中间件,缓存,ORM框架,他们都有一个特征,都是可替换的,比如一个数据库可以从mysql缓存oracle,缓存可以从redis换成memcache等等,这些都是属于业务域核心逻辑之外的东西,你对他们是没有控制权,所以你要做的事,即使这些外部依赖发生了变化,也能将自己的修改控制在最小范围,这就是系统的可维护性。
- 在service中直接调用了日志的RPC服务,如果后期日志服务升级,可能会影响登录逻辑。
- 在service中使用gorm框架直接从数据库查询用户,如果后期流量比较大,使用了缓存,需要先查询缓存,改动可能会影响登录的核心逻辑。
问题二:业务逻辑耦合严重
- 注册逻辑中耦合了参数校验,手机号码校验逻辑,如果后续需要新增检验逻辑势必会对核心逻辑进行修改。
- 注册逻辑中耦合了查询用户最后一次登录时间的逻辑,理论上这应该属于用户的行为。
DDD实现代码结构
DDD代码结构
- acl-防腐层,主要逻辑调用日志服务获取用户最后一次登录时间,发送短信逻辑。
- entity-实体、聚合根层,这两个我习惯放一起,因为聚合根就是聚合了实体和值对象的一种特殊实体。
- repository-仓库层,主要对应了数据库的操作以及数据库的schema,缓存操作等。
- service-业务逻辑编排层。
- valobj-值对象层
acl层代码
user_log_acl.go
sms_acl.go
entity层代码
user.go
repository层代码
user.go
service层代码
service.go
valobj层代码
username.go
这样做的好处
- 代码结构及业务逻辑清晰。。
- 所有用户域的逻辑全部内聚在User聚合根对象,方便复用及维护,单测起来非常方便。
- 防腐层的使用,不管外部怎么调整,都不会影响我登录的核心逻辑。
- 仓库的使用,后续orm框架的升级替换等等,都不会影响我登录的核心逻辑。
- 系统的扩展性,可维护下显著提升。