从软件复杂度的角度去理解DDD
Posted 轻风博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从软件复杂度的角度去理解DDD相关的知识,希望对你有一定的参考价值。
从我们作为业务开发主要的职责深入到DDD的本质是什么?复杂度应处理?规范设计怎么做?本文将全方位为大家解答。
一、作为业务开发,我们的主要的职责是什么的
业务开发的职责
这里摘录一下文章中要点
技术一号位是负责使用技术能力解决业务问题,提供稳定可靠的技术支撑; 负责向业务各方提供各种必要的技术支撑,通过合理的数据分析为业务决策提供依据; 通过对技术领域的积累和发展,通过业务领域的理解和落地影响业务决策; 负责构建梯队完整、能力全面、制度完善的技术团队来支撑业务发展。
业务在实际开展中遇到的问题
-
业务启动期:业务能力快速搭建 - 系统提供快速试错的能力 -
业务发展期:业务能力扩展 - 系统需要支持原来越多的业务功能 -
业务平台期:业务能力复制 - 系统需要支持原来越多的业务场景 -
业务衰退期:业务能力创新 - 系统提高生产力延长业务的生命周期
软件复杂度
-
高性能
-
单机性能 -
集群性能
-
高可用
-
计算高可用 -
存储高可用
-
可扩展性
-
低成本 -
安全 -
规模
-
业务规模 -
系统物理规模
二、DDD的本质是什么
DDD实施给系统之后,我们依然需要关注系统其它的复杂度,这里列举一些示例措施:
容量规划 架构设计 数据库设计 缓存设计 框架选型 发布方案 数据迁移、同步方案 分库分表方案 回滚方案 高并发解决方案 一致性选型 性能压测方案 监控报警方案
-
领域模型描述问题域的准确性
2、技术实现的复杂性
-
软件的可扩展性较差 -
软件变成面向过程 -
分层不合理 -
没有规范
那DDD是如何处理上面提到的软件复杂度的?
-
提供了一个领域划分的方法:让软件系统产生边界。 -
提供一个一系列的战略模式:限界上下文的映射,分层架构等。 -
提供一个一系列的战术模式:如何规划领域层 内部
DDD不是什么?
-
不光光只是一种编程方法 -
不光光只是一种架构风格 -
不具体指导如何具体建模
三、复杂度处理-领域模型描述问题域的准确性
合理性证明
什么是问题域
如何实现问题域的分析
-
关联模型与软件实现; -
基于模型提取统一语言; -
开发富含知识的模型; -
精炼模型; -
头脑风暴与试验。
1 四色原型法
2 用例分析法
问题域的拆分
1 通用域: 非应用独有的,多个应用都会有的功能。例如发送邮件,触达等
2 核心域:和竞争对手区别开来的区域,或者是在市场上被赋予了竞争优势的区域。
-
系统哪部分最难用 -
手动处理过程阻止了他们进行了根据创造性, 有附加值的工作 -
哪些修改能提高收益 -
哪些修改能提高运营效率
取哪些提示,取决于业务系统的性质。
-
卖家 + 会员身份:这两者都是核心实体,网站可能为了让促进会员能够多参与拍卖可能提供了分层,或者积分等工功能。网站为了能让卖家能够更加提供更加有拍卖价值或者是转化率高的品类可能为卖家提供了数据分析等业务功能。 -
名册:这也就是核心实体,网站会对名册提供一系列拍卖相关的功能,例如倒计时,一口价等,所以也需要形成一个领域。 -
拍卖:网站最核心的业务流程,核心域无疑。 -
争议解决:买卖家的售后冲突解决流程向来很复杂,所以会独立成为一个域无疑。
四、复杂度处理-进一步降低问题域的复杂度-限界上下文
限界上下文的诞生背景
限界上下文的本质
上下文映射
shared kernel 共享内核 shared kernel :通常是共享核心领域或者是一组通用子领域。 customer/supplier
客户/供应商关系 customer/supplier:上下游关系。不同客户需要协商来平衡,上游团队需要有自动测试套件。 conformist
跟随者模式 conformist:单方面跟随模式。上游的设计质量较好,容易兼容,可以采用严格遵循上游团队的模型。 anticorruption layer
防腐层 anticorruption layer:防腐层、隔离层,使用 facade or adapter 等模式。可以减少其它系统变动对本系统的影响。 separate way
各行其道 separate way:声明一个与其它上下文毫无关联的 bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案。 open host service
开放主机服务 open host service:开放子系统供其他系统访问。其核心思想是开放出一个标准的各个领域都认可的协议,减轻各个领域实施ACL的负担和成本。 published language
共享语言 published language:把一个良好文档化、能够表达领域信息的共享语言作为公共的通信媒介,必要时在其它信息与该语言之间进行转换。
-
shared kernel:如果使用共享二方库,谁来维护这个二方库,如何防止在不同上下文使用不同kernal版本所带来的问题。
如果一定能保证shared kernel的维护在一个团队内,且所有使用shared kernel版本一定能保持一致, 那是可以使用的。
-
customer/supplier:我曾经因为汇率包升级而去重构一个应用,因为汇率包变更太大,且应用没有防腐层,所以不论从开发还是测试都是非常痛苦的过程。 -
conformist:和customer/supplier类似, 但是在互联网领域没有靠谱的设计, 只有有人维护和没有人维护的设计。conformist从长期来看其实就是customer/supplier。 -
Open Host Service 没有任何一个领域保证自己的接口一定不会变,就算不会,其他领域的同学会相信吗,他们会忍住不用ACL吗?如果他们用ACL,OHS的意义何在? -
publish language 目前阿里内部MTOP,TOP等协议正是使用这样的协议。
另外限界上下文之间真的能够随便无规则无条件的互相依赖,互相调用吗?在下面的章节将会解释论述。
五、复杂度处理 - 分层不合理
传统的三层架构
六边形架构
洋葱架构
DDD 架构
CQRS
我们团队里面的架构实践
1、依赖关系(除了依赖倒置)只能是从上当下;
2、同层之间永远不能互相依赖;
六、复杂度 - 软件变成一个大泥潭
实体
实体建模的注意点
-
现实意义标识符 -
人工生成的标识
-
自增 -
guid/uuid/ -
数据库主键 -
自定义sequence
2、验证和不变行
规模模式:
-
只是用id引用 -
value objects
什么时候需要使用到值对象?
-
概念需要凸显的时候。
public class WinningBid ... public int Price get; private set; ...
-
表述一个描述性的,但是没有实体编号的概念的时候。
值对象的特征
-
无标识:他们只是标识对象的属性。 -
基于属性的相等性: 所有的属性值相等即值对象相等 -
富含行为:值对象实现业务概念的抽象,其也有自己的行为 -
内聚:将不同的相关属性组成一个概念整体,例如Money, 是由一个long 和一个currency组成的 -
不变性:值对象是不变的对象,如果需要改变属性,那最好是建立一个新的对象并且进行值对象替换。如果一定是需要改变,那就需要考虑设置为值对象是否合理。
不变性是值对象非常重要的一个属性,是可以保障值对象不会被"坏味道"代码侵入的一个原则之一。 例如如果一个值对象引入了另外一个类实例, 另外一个值对象也引入了相同的类实例, 如果值对象允许改动,当一个值对象对这个类实例的内容进行修改,势必会影响另外一个值对象。 所以最安全的方式还是通过对象替换的方式。
域服务
什么时候用域服务
域服务应该包含什么内容
应用服务与领域服务的区别:
应用服务里不要处理业务逻辑,只在领域服务里处理业务逻辑。(如何判断某段逻辑是否是业务逻辑?) 领域服务掌握领域知识,而应用服务只是对领域服务的编排。 应用服务是领域服务的客户方,也就是说应用服务会调用领域服务里的方法。 当领域中的某个操作过程不属于实体或者值对象的职责时,需要将个操作放在领域服务中。而且确保领域服务是无状态的(这句话很有意思,也就是说领域服务中不应该有任何记录状态的行为,在任何情况下调用这个服务,它都不会有副作用,也就是说它是个纯内存操作)。 领域服务中包含的是业务逻辑,而应用服务关注的应该是安全和事务等非业务逻辑。 对事务的管理绝对不能放在领域服务层,事务管理需要放在应用服务层。因为和领域模型相关的操作的粒度都很细,无法用于事务管理。而且领域模型也不应该意识到事务的存在。 通常的可以放在应用服务中的逻辑有:参数验证、错误处理、监控日志、事务处理、认证与授权。
A ->B
A.accountDecrease(10);
工厂
-
只负责复杂逻辑对象的构建,让构建逻辑中心化 -
减少外部对象对构建对象内部变量的理解 -
工厂方式不是在任何构建对象的时候使用,一定用在对象构建逻辑复杂,有子依赖或者是有invariant规则的场景。
Repository
-
uniqe ID 的生成 -
数据库的操作 -
数据模型到领域模型的相互 -
横跨多个数据模型构建出一个实体模型。
repository的反模式
-
定义出比较通用化的接口, e.g.
List<Customer> findBy(CusomterQuery query)
-
使用了延迟加载,延迟加载就是设计错误的标志, 有可能说明我们聚合的边界不催。 -
不要为了报表的诉求使用reponsitory, 领域的case和业务报告很不一样,可能需要多个聚合的数据,这种情况可以考虑用一个离线的store去做,和其他的读服务去做,不见得一定需要用领域的Repository模式。
领域事件
-
google guava - EventBus -
COLA框架 - 事件支持
聚合是什么?
如何划分聚合
聚合设计的原则
-
聚合是用来封装真正的不变性,而不是简单的将对象组合在一起; -
聚合应尽量设计的小; -
聚合之间的关联通过ID,而不是对象引用; -
聚合内强一致性,聚合之间最终一致性;
什么是聚合根
如何选出聚合根
1、聚合根一定至少有一个对应的datastore
2、聚合根一定更够完全描述一组后者多组业务规则(invariant)。绝对不会存在一个业务规则需要多个聚合根联合作用才能做判断的。
七、规范设计
放对位置
-
程序架构目录
贴好标签
-
类名约定 -
方法名约定 -
错误码约定 -
Domain Event约定 -
测试约定
下面是我们小团队参考了阿里COLA框架建议的命名规范做出的开发规范:
程序架构组织
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
从需求的角度去理解Linux系列:总线设备和驱动笔者成为博客专家后整理以前原创的嵌入式Linux系列博文,现推出以让更多的读者受益。 《从需求的角度去理解linux系列:总线、设备和驱动》是一篇有关如何学习嵌入式Linux系统的方法论文章,也是从需求的角度去理解Linux系统软件的开篇,期待此系列文章日后会是学习嵌入式Linux的标杆!
这是作者精心撰写的经验总结,希望嵌入式Linux的学习者仔细领会,多读几遍也无妨。
四、总线、驱动、设备
隆重推荐本人以下原创文章,有助读者系统、全面地理解嵌入式Linux的系统架构和驱动开发! 从需求的角度去理解Linux系列相关博文: 3. 符设备驱动字、平台设备驱动、设备驱动模型、sysfs的关系
5. Linux中断完全分析 7. 全网络对Linux input子系统最清晰、详尽的分析 8. 陆续推出Framebuffer、I2C、MTD等子系统的分析
微信公众号:嵌入式企鹅圈 1.忠于Linux源码,百分百原创。 2.从上电第一行代码、系统第一行代码、模块第一行代码、应用第一行代码,深入讲解嵌入式软件生命周期。 以上是关于从软件复杂度的角度去理解DDD的主要内容,如果未能解决你的问题,请参考以下文章 |