技术

并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go细节 codereview mat使用 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 MVCC 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes自动扩容缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 《阿里巴巴云原生实践15讲》笔记 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 扩展Kubernetes 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占cpu怎么办? 容器狂打日志怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 自动化mock AIOps说的啥 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 marathon-client 源码分析 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 swagger PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 mesos 的一些tips mesos 集成 calico calico学习 AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 硬件对软件设计的影响 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 从一个marathon的问题开始的 docker 环境(主要运行java项目)常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 network channel network byte buffer 测试环境docker化实践 netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 mesos深入 Macvlan Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty(六)netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 那些有用的sql语句 异构数据库表在线同步 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM内存与执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp mesos简介 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 Kubernetes 其它特性 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 CoreOS 使用 Go学习 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

标签


领域驱动理念入门

2017年12月25日

简介

革新的对象——面向数据库的架构

领域驱动设计(DDD:Domain-Driven Design)提到服务器后端发展三个阶段

  1. UI+DataBase的两层架构,这种面向数据库的架构没有灵活性。
  2. UI+Service+DataBase的多层SOA架构,这种服务+表模型的架构易使服务变得囊肿,难于维护拓展,伸缩性能差
  3. DDD+SOA的事件驱动的CQRS读写分离架构,应付复杂业务逻辑,以聚合模型替代数据表模型,以并发的事件驱动替代串联的消息驱动。真正实现以业务实体为核心的灵活拓展。

领域驱动设计和开发实战不投入资源去建立和开发领域模型,会导致应用架构出现“肥服务层”和“贫血的领域模型”,在这样的架构中

  1. 外观类(通常是无状态会话 Bean)开始积聚越 来越多的业务逻辑,而领域对象则成为只有 getter 和 setter 方法的数据载体。
  2. 这种做法还会导致领域特定业务逻辑和规则散布于多个的外观类中(有些 情况下还会出现重复的逻辑)。
  3. 在大多数情况下,贫血的领域模型没有成本效益;它们不会给公司带来超越其它公司的竞争优势,因为在这种架构里要实现业务需求变更,开发并部署到生产环境中去要花费太长的时间。

基于数据库设计

多视角理解领域驱动

分解复杂性视角

领域驱动设计在互联网业务开发中的实践 解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识。

  1. 分治 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。
  2. 抽象 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。PS:《原则》中也有类似的表述,你在思考高层次的事情时,一定不要考虑低层次的细节。《重构》中讲一个方法只要 包含跟该方法同级层次的代码。
  3. 知识 顾名思义,DDD可以认为是知识的一种。DDD提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。

西瓜可以横着切也可以纵着切,分治怎么分也要找到一个切口。

在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似MVC这样,业务维度则是指按业务领域来划分系统。微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构

领域驱动设计学习输出「DDD」则把大多数的业务逻辑都包含在了「聚合」、「实体」、「值对象」里面,简单理解也就是实现了对象自治,把之前暴露出来的一些业务操作隐藏进了「域」之中。每个不同的区域之间只能通过对外暴露的统一的聚合根来访问,这样就做了收权的操作,这样数据的定义和更改的地方就聚集在了一处,很好的解决了复杂度的问题。

软件架构设计视角

为什么是“领域”驱动,而不是什么别的东西驱动?比如服务驱动?对象驱动?

  1. 简单的系统数据库CRUD就可以搞定,只有足够复杂且多变(二者缺一不可)的系统才用得上领域驱动
  2. 开发人员经常把业务流程实现成系统流程,业务流程复杂、多变的时候, 系统流程也必须做出改变,因而需要在“业务流程”和“系统流程”之间提出一层,即领域模型
  3. 领域就是现实世界的业务,是复杂多变的,我们看到的只是现象。而领域模型就是要找到这些现象背后不变的部分,也就是本质,也就是“变化”背后的“不变性”
  4. 就像任何一门语言,最基本的是单词。领域驱动设计的一系列概念:实体、值对象、聚合根、领域事件、Specification,就是领域模型这门“建模”语言的“单词”。给了我们一系列分析工具,帮我们分析出“领域”现象背后的“本质”。

所以,换句话说,本质是业务流程和系统流程不一致带来许多问题, 需要抽一个中间层,这个中间靠近业务/领域,所以以“领域”方式描述,但又不能易变,所以必须找到业务中不变的部分(即本质),来减少系统流程的变动

如何用代码有效描述业务视角

领域驱动设计学习输出DDD 改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为通过合理运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。

阿里盒马领域驱动设计实践

  1. 传统项目中,架构师交给开发的一般是一本厚厚的概要设计文档,里面除了密密麻麻的文字就是分好了域的数据库表设计。言下之意:数据库设计是根本,一切开发围绕着这本数据字典展开
  2. 我经常会做一个假设:假设你的机器内存无限大,永远不宕机,在这个前提下,我们是不需要持久化数据的,也就是我们可以不需要数据库,那么你将会怎么设计你的软件?这就是我们说的 Persistence Ignorance:持久化无关设计。首先一点,领域模型无法通过数据库表来表示了,就要基于程序本身来设计了。
  3. 按照 object domain 的思路,领域模型存在于内存对象里,意味着得 通过 类图 而不是ER图来描述业务。用类 比用 数据库表 有更丰富的表达方式:通过引用来表达多对多关系、封装、继承和多态等。

领域驱动设计和开发实战从设计和实现的角度来看,典型的 DDD 框架应该支持以下特征:应该是一个以 POJO(这里说的应该是充血对象)为基础的架构;领域第一,基础设施第二:PersistentObject只是表达了一种存储方式而已, 跟业务毫无关系。

领域模型设计

领域模型:失血、贫血、充血

我们必须将应用程序的业务逻辑从服务层迁移到领域模型类中,为何呢? 先来看看贫血模型和充血模型的对比。

假设我是一个服务类,你是一个域模型对象。如果我让你从屋顶上跳下来,你会喜欢我这样的决定吗?跳下来会摔伤,自己没有脑子或被洗脑,变成僵尸,只听从执行,不思考自己的安全,这就是贫血模型的问题。

举个具体的例子,假设一个用户有很多收货地址

class User{
	List<Address> addresses;
	setter
	getter
}

那么在为用户添加收货地址时,不得不有很多判空操作

class UserService{
	void addAddress(User user,Address address){
		List<Address> addresses = user.getAddresses();
		if(null == addresses){
			addresses = new ArrayList<Address>();
			user.setAddresses(addresses);
		}
		addresses.add(address);
	}
}

想象一下

  1. 如果有多个位置操作User的Address(这个例子针对这一点不是很适当),if(null == addresses){...} 会大量出现,代码量不大, 但会很丑。如果是电商业务,每一次购物都要做优惠券、红包、满减检查、余额不足检查等,这些逻辑有可能重复在各个Service中。
  2. 更复杂的成员变量 List<List> 或者 List<Map<String,String>>
  3. 更复杂的逻辑,比如设定默认地址,地址判重等。

UserService.addAddress吐血表示,我只想添加个地址而已。

换成充血模型

class User{
	List<Address> addresses;
	public User(){
		addresses = new ArrayList<Address>();
	}
	void addAddress(Address address){
		addresses.addAddress(address)
	}
}
class UserService{
	void addAddress(User user,Address address){
		...
		user.addAddress(address);	
		...
	}
}

从中可以看到,addresses的 初始化和 添加都由User 负责,代码简洁很多。

个人体会:

  1. 除了列表等简单场景,一定要避免ViewObject、BusinessObject/DomainObject、PersistentObject一对一,最起码ViewObject 不应该和PersistentObject 有直接的关系 ,否则代码中必然有一堆的对象转换工具类。
  2. PersistentObject一般由框架自动生成,不适合做改动,只提供setter/getter方法,或者说除了set/get什么都做不了。这样不得不很多逻辑放在XXService中,造成XXService的臃肿。直接暴露set/get很多时候是有不安全的。
  3. BusinessObject仅提供跟业务强相关的方法,很多XXService的事情建议挪到BusinessObject做,XXService只负责调用BusinessObject 以及处理 事务一致、权限检测等非业务逻辑。
  4. 数据类不只存在于PersistentObject中,如果一个类,外部对这个类的使用就是处理它的成员而跟类本身没关系时 都可以认为是数据类。
  5. 类尽量只对外暴露能力,而不是暴露数据。观察使用这个类的常见场景(包括输入和输出两个方向),提炼为方法放在这个类中,尤其是可以多应用函数参数。此时,该类内部的成员的数据结构如何变化便不重要了。

读写分离

DDD读写对待不一样的,写需要严格遵守分层结构。读不一定,看情况。

领域模型是用于领域操作的,当然也可以用于查询(read),不过这个查询是有代价的。在这个前提下,一个 aggregate 可能内含了若干数据,这些数据除了类似于 getById 这种方式,不适用多样化查询(query),领域驱动设计也不是为多样化查询设计的。 查询是基于数据库的(比如 获取某数据的列表,这是一个查询需求,不算业务模型之内。业务模型一般侧重于 几个抽象 以及 抽象之间的相互作用),所有的复杂变态查询其实都应该绕过 Domain 层,直接与数据库打交道。

横着看

DDD不只是指导写代码

领域驱动设计学习输出面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构与六边形架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入到限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。若在实现过程中,发现领域模型存在重复、错位或缺失时,再进而对已有模型进行重构,甚至重新划分限界上下文。

小结

其实简单点讲,就是传统方式 本质为 以数据库表来描述 业务模型,而领域驱动 以 类图(类及相互作用关系)来描述业务模型。明显的,表达手段越多(类图丰富的表达手段,比如继承、多态等) ,便越容易 描述复杂事物,也更容易应对变化。

2018.6.20 补充 大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的 本文字字珠玑,适合细读。

你写的代码是别人的噩梦吗?从领域建模的必要性谈起

领域驱动设计在互联网业务开发中的实践

DDD领域驱动设计基本理论知识总结

领域驱动设计示例