DDD权限平台建模与实战(附代码)
神帅的架构实战
Java 资深开发工程师
读完需要
9分钟速读仅需 3 分钟
1
背景
在与很多微信朋友讨论 DDD 落地的时候也给出了一些自己的见解,但是却很少有机会亲手尝试下如何解决一些现实场景,因此借着 DDDinAction 的项目通过权限平台的代码实战将自己的思路一点点落地和完善。
当然也有很多人希望找到一个 DDD 实战项目代码,因此在迭代了一些 codeMaker 的功能特性之后便把 infosys-plat 的 infosys-auth 权限平台来真正落地一把,看一下用 DDD 的方式如何写出更好的代码。
2
权限平台需求
2.1
总体需求列表
1. 构建统一权限平台对接全公司的权限应用场景
2. 基于 RBAC 实现权限模型
3. 提供 web 管理平台的 API 接口,支持 dubbo,springboot api,提供 SDK 的 jar 包接入鉴权
4. 对角色,权限,用户,系统菜单等统一管理
5. 第一版本实现核心业务模型的 CURD,部分导入导出功能
特色功能实战(待实现):
6. 打通审批流平台实现自动权限审批
7. 支持细化的数据权限规则应用,如某用户只能访问某个列表的最近三个月的数据
8. 临时授权,比如某用户给另外一个用户开通临时授权,到期收回
9. 权限操作记录日志审计组件接入
10. 权限风险审计
2.2
系统参与者
角色 | 使用功能 | 说明 |
权限平台超级管理员 | 权限平台所有功能 | 可能需要单独的表存储超级管理员配置 |
业务线管理员 | 配置角色和相关系统菜单资源 | 由超级管理员配置 |
各个接入系统用户 | 权限验证和管控 | SDK接入 |
2.3
业务流程
在本项目的演示案例中着重找了两个比较重要的业务流程来看一下业务时序图
1. 权限构建流程
2. 用户鉴权流程
3
权限模型
3.1
权限上下文分析
3.2
权限领域模型文档
这里需要说明的是本项目不会有太多的建模过程,对相应的场景不做过多的识别,尽量保障一篇文章可以讲完整个实战内容,所以会直接的给出相关领域建模文档。当然如果你看代码有些疑问的话也欢迎交流讨论。
3.3
实体划分
实体名称 | 实体描述 | 实体行为 |
AuthorityBO | 权限模型 | 禁用,启用,判断是否具有某项类型的权限资源 |
DataAuthorityBO | 数据权限模型 | |
SystemAuthorityBO | 系统权限模型 | 构建系统权限 |
AdminAuthorityBO | 行政权限模型 | |
UserAuthAggregateBO | 用户权限角度的聚合模型 | |
RoleAuthAggregateBO | 角色权限角度的聚合模型 | |
UserGroupBO | 用户组模型 | 判断用户组是否关联了指定角色 |
RoleBO | 角色模型 | 禁用,启用,关联用户列表 |
RoleGroupBO | 角色组模型 | |
RoleUserBO | 角色用户关联模型 | |
SystemBO | 系统模型 | 添加模块 |
ModuleBO | 菜单模块模型 | 添加按钮 |
MenuBO | 按钮模型 |
3.4
值对象划分
值对象 | 值对象说明 | 备注 |
DataColumnBO | 数据字段模型 | 有增删改查等,但是看上去更像配置类对象,在这里对配置对象统一称作值对象 |
AuthorityTypeEnum | 权限类型 | 枚举对象,在枚举类中,统一为值对象 |
UserBO | 用户对象 | 用户对象在用户服务中算是业务对象,但是在权限系统里只是依赖这个对象实现模型和业务的完整性,用户状态等并不归权限管,所以这里可以看作是值对象,但是是在domain.support包下,注明是支撑域下的对象。 |
DepartmentBO | 部门对象 | 与用户对象一样 |
Address | 省市县对象 | 这个对象算是复合对象,但是只是权限聚合的一部分,并没有实际行为和状态,所以可以看作是值对象。 |
这里需要说明的是在本项目演示中并没有过多的对值对象的表达做演示,比如对 userId 则用 UserID 对象表示,当然也有一些大佬的演示文章会这么做。在本项目中就是对值对象采用最基本的数据类型进行表达,尽量不增加对象的遍历和依赖深度。
在实践过程中我们可能无法过多的对值对象能产生的一些规则进行关注,而不是简单的对识别出来的一些属性做对象封装,这无疑会让整个业务对象变得有点支离破碎,同时增加理解业务的难度,实践起来也会带着 DDD 的一些概念被桎梏。后面有机会就具体聊一下这方面的内容。
所以大部分场景下的值对象其实不需要完全使用对象包装,在我看来如果你意识到这是值对象或者你能划分出来某个对象或者数据结构是值对象就行了。用基本类型封装并不意味着我们无法表达他的规则和业务场景,所以不必过分在领域层特别标明这是个值对象,比如 XXVO 这样的,但是需要注意的是要通过文档来表达哪些对象是值对象。
3.5
聚合根识别
聚合根 | 聚合根描述 | 聚合作用 |
AuthorityBO | 权限,对整个角色对应的权限做统一关联处理,内部是数据权限,行政权限和系统权限 | 角色可以关联很多权限资源,这些权限资源目前识别了三类,后续可以自定义权限资源,也就是说在这个聚合里也有资源的这个对象,只是没有特别关注到,算是隐喻。所以权限约等于资源。 |
UserAuthAggregateBO | 基于用户角度的权限聚合模型 | 由于是RBAC模型,所以我们无法直观的看到用户有哪些权限,那么这个权限就相当于一个读场景的快照聚合,写仍然是通过角色管理权限资源 |
RoleAuthAggregateBO | 基于角色角度的权限聚合模型 | 在进行读的时候因有了整体的AuthorityBO做聚合管理了,那么这个角色可能会对应多个AuthorityBO,所以需要通过角色维度来看这个角色有多少权限资源。 |
SystemBO | 基于系统菜单维度的聚合模型 | 这个相当于一个省市县的模型,重点是这个模型是动态变化的,而且权限系统比较依赖他,但是维护在权限系统是因为这是个核心的权限资源,所以对权限和系统菜单本身来说系统就是一个整体的聚合对象,系统本身需要屏蔽内部菜单和按钮,外部依赖的则是系统及其内部的菜单按钮的业务标示做读依赖。 |
4
权限领域服务
4.1
领域服务文档
这个文档体现了用DDD与不用DDD的最大的区别,因为面向上下文面向聚合来构建服务接口的话相当于对底层数据表和相关服务做了整体的抽象和归纳,因此看上去不会是一个表一个接口一个服务类的那种。由于对不同的场景做了专门的区分,同时使用了依赖倒置的方式让应用层不会去跨层调用基础设施层,整体上可以做到可扩展,灵活性高,维护和修改也会变得比较轻松。
4.2
领域能力表格
上下文 | 对应模块 | 能力说明 |
用户组 | user | 对某一类用户进行分组管理 |
角色组 | role | 对某一类角色进行分组管理 |
角色 | role | 维护角色及其关联的角色-用户关联关系 |
系统 | system | 维护系统及其菜单按钮的相关资源 |
数据字段 | config | 提供数据权限的相关的元数据信息管理维护 |
权限 | authority | 提供统一权限资源的抽象能力,解耦角色和具体资源 |
行政权限 | authority | 提供行政相关权限资源的配置关联 |
数据权限 | authority | 提供数据相关权限资源的配置关联 |
系统权限 | authority | 提供系统相关权限资源的配置关联 |
5
权限平台 Cola 应用架构
5.1
权限应用架构
5.2
防腐层模式的两种实践
1. 在领域层对下游依赖接口进行一次接口方法封装算是领域网关的一个实现方式,比如对缓存操作的依赖,对下游其他接口的依赖封装,返回和请求的对象都算在领域中,只是需要与领域内核心模型区分开。
2. 第二种是整个领域依赖的业务性下游服务,比如用户中心的用户接口,部门接口等。这种事在基础设施层的 acl 包中构建依赖的下游接口方法,内部实现调用下游接口逻辑,并按领域对象进行返回,请求对象可以由领域内对象 BO 转换为 DTO。
以上两种方式各有优缺点,如果在领域层进行封装那应用层可以在某种程度上跨国领域层直接访问下游接口。如果在基础设施层构建的话可能需要考虑对象的转换,以及领域内对下游方法的业务处理。
5.3
CQRS 模式应用
在本工程里面对核心的业务模块做了读写分离,提供读和写两套接口,同时在领域层也对读作了专门的分离。在 app 层通过 command+executor 的方式对权限相关业务流程作专门的应用层处理,比如给角色授权等等。
5.4
CQE 模式应用
在领域层中对 bo 下的各个数据业务实体作了专门的分类,比如 BO, EVENT, MsgBody。因此在应用层通过 command+executor 的方式控制业务应用的时候会通过应用层的 CMD 对象来转换 Evevnt。这样的一个好处是可以做异步化。
代码演示案例如下:
这里需要多说一点的是关于事件的应用其实有好几种,下面看一下:
领域内事件(在领域服务内部产生的事件)
应用层事件(应用层的一些事件也跟事务有关),带事务处理特性的事件和其他监听事件
事件延伸的消息(比如因为某事件需要发送消息,或者接收消息)
关于上面的一些事件有时候会因为业务特性采用同步操作,有时候则会使用异步来实现,但是需要注意的是由于事件的应用场景有很多,过多使用可能会造成一定的复杂度,无法保证整体业务的连续性,毕竟代码是要给人看的。一个可行的方式就是对不同的事件做专门的区分,同时将事件与事件处理器尽量显示的关联起来做动态配置化路由。
5.5
规格模式应用
在使用规格模式之前我也专门回顾了下 eric 的书。在应用层的系统菜单按钮的查询场景下做了一次尝试。看上去效果不错,在应用层的 SystemQueryFacadeImpl 中构建了几个简单的查询接口,同时通过规格模式来判断不同的查询条件是否满足,这样的话对外接口数则变得非常少,同时对其他模块的读逻辑可以做收拢。这里看下代码案例:
内部通过规格路由即可将不同的查询场景做收拢。但是需要说明的是在 eric 的书中对规格模式的应用是在领域层的,相当于在领域层对比较复杂的连表查询或者统计查询作了不同规格的处理。所以之前有群友说规格模式应用在哪一层,我的建议是应用层和基础设施层都可以。当然如果是领域层的话,按照 eric 的书来实践的话领域层可能就需要单独的类来构建查询 sql 和查询对象了,所以不同的实践你看到的 DDD 代码其实也不一样。
5.6
独立类模式应用
因为权限模型比较复杂,所以这里构建整个权限数据的话肯定会不少,所以需要设计下缓存相关的逻辑,因此在领域层中定义了不同业务模块的缓存前缀,在基础设施层构建不同业务对象的缓存服务类。由于应用层无法直接到基础设施层引用缓存服务类,所以为了保持整体分层架构的一致性,这里在领域网关的包里定义了一个 CacheServiceGataWay 接口。通过不同的业务对象标示来在接口实现方法内部进行路由调用。
具体代码则不截图了,感兴趣的话可以 down 下看看。
5.7
奥卡姆剃刀原理应用
在写代码的时候一开始是采用的 codeMaker 生成的不同模块的代码类,但是实际应用中对于聚合的把控会让一些生成的代码类变得非常尴尬,比如应用层的 facadeimpl 中的不同权限类型下的接口实现,因为聚合的控制这些不同的权限接口变得相对冗余,因此通过@Deprecated 注解将其剃掉,相应的 CURD 走 Authority 聚合接口。
当然类似的情况也在 auth-adapter 中出现了,所以大家看代码的时候不用惊讶,这样对比着看才有好坏之分。
5.8
DDD 严格分层架构
这里因为采用了 Cola 架构,将 dubbo 接口的实现放在了应用层,所以看上去与 springboot 的接口实现依赖的领域层和应用层不是很协调,当然现实情况是不会存在一个项目里有两套不同风格的对外 API。那这里我要重点说明的是在本项目中是不会有应用层跨过领域层实现调用基础设施层接口的情况的。
当然在 auth-adapter 模块为了保障其本身作为用户接口层的职责之后,本来是需要通过应用层来访问领域层,但是在项目里应用层是 dubbo 的实现层,所以在 auth-adapter 访问了应用层和领域层。注意这里访问了应用层是因为在某些模块希望能引用用到 command+executor 类。如果没有 dubbo 实现的话,那么整体应用层将单独为 auth-adapter 服务。不会存在这个特殊的跨层调用。
5.9
服务依赖说明
对接审批流接口实现自动权限审批能力
对接用户中心获取用户和部门数据
对接省市县数据服务
6
代码项目说明
6.1
项目主页
在这里统一说明一下,我实战 DDD 的代码基本上都在这个项目里:https://gitee.com/codergit.com/dddin-action 本次迭代发布的工程内容有两个:
infosys-plat 工程下的 infosys-auth 平台代码
youpinshop 工程下的 stock-simple-demo(扣库存的各种视角演示案例)
6.2
项目内容
提供各个业务对象的基本增删改查
提供最小中间件依赖的环境,方便启动项目(理论上只依赖数据库),集成 cache,mq 也比较方便,内部默认对这些实现了空内容,少量开发即可实现完整版本
提供 dubbo 接口和 spring boot 接口实现
提供领域模型文档,领域服务文档,DDL 文档
在项目代码中详细增加处理各个场景的说明注释
7
总结
7.1
总结
总体底座代码由天画-codeMaker 支持,直接填充业务方法内容即可。
提供 auth-common 包,不需要再依赖 coderman-utils,整体依赖闭环。
对不同场景做针对性理论说明,输出不同处理方案来控制项目复杂度,对最近的 DDD 理论学习做代码实战。
在实现的过程中也有很多困惑,所以也请教了一些资深大佬,同时根据自己的理解在项目中构建一些可行的方法思路。
7.2
彩蛋(讨论点)
RoleUserBO 这种关联关系对象算实体还是值对象?
DO 模型对多表连接查询,统计查询的兼容性有多少,是不是要单独构建数据实体还是复用?
mapstruct 如何解决不同数据类型之间的映射,比如 str->list,父类属性转到子类属性?
往期推荐