技术

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

架构

分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF 生命周期管理 openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


ddd反模式——CRUD的败笔

2018年11月13日

简介

浅谈我对DDD领域驱动设计的理解 很多项目(尤其是互联网项目,为了赶工)都是一开始模型没想清楚,一上来就开始建表写代码,代码写的非常冗余,完全是过程式的思考方式,最后导致系统非常难以维护。我们今天吐槽一下controller-service-dao的“坑”,挖一挖它的墙角。如果你觉得controller-service-dao 很不错,那说明你应对的场景还不够复杂,暂时还不适合谈论ddd。

很多项目的实际情况

  1. 用户或产品经理需求零零散散,不断变更
  2. 工程师在各处代码中寻找可以实现这些需求变更的代码,修修补补
  3. 软件只有需求分析,并没有真正的设计,系统没有一个统一的领域模型维持其内在逻辑一致性
  4. 功能特性并不是按照领域模型内在的逻辑设计,而是按照各色人等自己的主观想象设计。

CRUD/controller-service-dao的败笔

Spring Web 应用的最大败笔

大部分Spring的Web应用程序,常见的错误的设计如下:

  1. 领域模型对象用来存储应用的数据(当作DTO使用),领域模型是贫血模型这样的反模式。
  2. 服务层每个实体有一个服务。

该应用程序有一个整体的服务层(Controller 仅负责绑定路由),它有太多的责任。更具体地,服务层有两个主要问题:

  1. 在服务层发现业务逻辑,业务逻辑被分散在各个服务层
  2. 每个领域模型一个服务。每一个类都应该有一个责任,不应将原属于领域模型的行为方法等划放在服务中实现,对象不但有属性还有行为。

领域驱动设计在互联网业务开发中的实践在我们习惯了J2EE的开发模式后,Action/Service/DAO这种分层模式,会很自然地写出过程式代码,而学到的很多关于OO理论的也毫无用武之地。使用这种开发方式,对象只是数据的载体,没有行为。以数据为中心,以数据库ER设计作驱动。 PS,对这句深有体会,此时一个系统最有含量的部分就是数据库设计,数据库表定了,剩下的就是腾挪数据了。

阿里盒马领域驱动设计实践 形容这类代码“面条代码”,从(客户)端上一条线杀到数据库完成一个操作,仅有的一些设计集中在数据库上。

领域驱动设计详解:是什么、为什么、怎么做?分层并没有问题,但是这种分层架构采用的是包的形式进行的层与层的隔离,需要每一位开发同学理解并且自觉遵守以上规范,但是在实际工作中我们发现很多同学对Service层和Manager层的区别并不是特别的清楚,即使清楚的同学大部分也并没有完全遵守手册中的规范。在实际的业务代码中Service层充斥了大量的第三方依赖,对系统的稳定性有很大的影响。每依赖一个第三方服务都要各种异常处理,这些异常处理的代码往往会和业务代码混在一起,当这种代码多了以后会使代码的可读性非常差。

领域驱动设计学习输出「CRUD工程师」认为自己没有创造任何东西,他们只是数据库表的搬运工。而如果不是「CRUD」,业务系统后端工程师的价值在哪里理解并抽象出业务逻辑,建立满足需求的业务模型,以此设计实现出可靠的系统,并有效地控制复杂性。这才是大部分业务系统后端工程师的工作重点,也是解决他们工作中遇到的问题和难点的关键。

写代码精进之路:快反而是慢?巧用DDD设计,慢中变快!

  1. 基于“Service + 贫血模型”的实现。业务逻辑复杂了,业务的逻辑、状态会散落到大量方法中,你没有抽象,就没有办法模块化,就不能区分核心和周边,需求越来越多,你就只能硬写,你的这种硬写,往往都是写到了核心模块里面了,之所以成为核心,不就是希望你不要总是改变它吗,要尽可能将其变为只读的,否则,你当初的快就是后来的慢。PS: 做过一段时间的技术leader,一个比较好的设计就是 小伙伴接手你代码时候,新需求他能看出来塞在哪里,能看出来大纲,预留空间大,小伙伴填充细节就可以了。你一篇文章从头写到尾,他还需要提炼关键步骤和思想。
  2. 为什么总是习惯用上面那种方式编写代码呢?可能是业务简单到就是基于SQL的CRUD。可能是在service层中可以定义任何操作。如何应对变化,如何不让当初的快,变成后面的慢呢。就是要千方百计地将核心模块和周边模块,变成正交性的设计,让核心模块变成只读,每次来一个需求只需要修改或增加周边模块就好了。那如何才能一步一步实现正交设计的代码呢,最原始的基础就是要用丰满的面向对象技术,用丰满的面向对象技术的基础方法又是充血模型。
  3. 应用基于充血模型的 DDD 的开发模式,需要事先理清楚所有的业务,定义领域模型所包含的属性和方法;领域模型相当于可复用的业务中间层;新功能需求的开发,都基于之前定义好的这些领域模型来完成;越复杂的系统,对代码的复用性、易维护性要求就越高,就越应该花更多的时间和精力在前期设计上;而基于充血模型的 DDD 开发模式,正好需要前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发;
  4. 第一次就需要考虑那么的周全吗?第一次就需要面向未来设计吗?我个人的建议,你可以被子弹打中一次,但是不要被打中第二次。如果你第二次,第三次依然没有抽象出领域模型,你的每一次以为的快,都是为后面每一次的慢,埋下了“因缘”。有没有好的策略,来指导如何判断要不要搞成所谓的领域形式呢
    1. 判断是否你的程序只为一个业务方服务。比如财务人员要用到、营销人员要用到、运营人员要用到。如果是,就要提前考虑沉淀出业务领域模型。
    2. 判断是否你的程序只为一个业务模式服务。比如拼团业务要用到、国际业务要用到、健康业务要用到。如果是,就要提前考虑好业务身份的判断且抽象共享服务。

搞得好像一切为了持久化

笔者在一篇文章中看到一个问题:如果内存足够大,且永不宕机,你还会用数据库么?不会, 因为:

  1. 数据库表不支持继承和多态,表达能力有限。假设用户的联系方式可以是邮箱、电话(包括国家码,后续可以考虑扩展支持运营商信息)、qq任意一种,则用对象表示

     class User{
         Contact contact;
         setter
         getter
     }
     class Contact{
         int contactType
     }
     class QQ extends Contact{
         String qq;
     }
     class Phone extends Contact{
         String country;
         String phone;
     }
    

    用数据库表示就很尴尬了,因为多态的感觉不太好弄,你只能:

    1. 建一个contact表,所有的字段都放在里面
    2. 建一个contact表,一种联系方式建一个表
  2. 表达一对多关系要额外加字段,表达多对多关系要额外建一个表

我们回想一下controller-service-dao的实现过程

  1. model + dao 借助自动化工具生成
  2. 有一个添加地址的需求
  3. 然后controller实现,进而在UserService 里加一个addAddress方法,进而自然地 逻辑就写在UserService.addAddress 里了,直到调用dao 为止。

搞得我们一切操作像是为了持久化,持久化是编程的目的么?有时候不是

还以上文的User为例,对每一个新来的用户,我们需要保存用户身份信息(身份证号、性别等)、收货地址信息、画像信息等。为了用户操作友好

  1. 用户信息 按类别 在不同的页面上输入。比如填完身份信息,点击下一步,让用户填写收货地址信息。
  2. 用户可以添加任意多个收货地址,可以让用户在地图上选择地址,考虑到页面空间有限,一个页面只添加一个收货地址。一个收货地址添加完毕后, 用户可以选择下一步(添加兴趣信息)或者 新增下一个收货地址。
  3. 每一个操作 都可以上一步,以便用户修改

针对这个需求,有几个实现方式

  1. 每一步操作都保存到数据库,回显时从数据库中读取数据。这涉及到 用户请求对象 和 数据库对象的 相互转换。
  2. 内存中有一个User 充血对象,在最后一步保存到db之前,其它所有的步骤只操作User 即可,包括但不限于

    1. 添加/回显身份证信息
    2. 添加/回显收货地址
    3. 添加/回显联系方式

为简单起见,你甚至可以将每一个步骤中页面发你的请求 数据直接保存在user 中,回显时原封不动直接返回给页面(用户的修改类似)。只有在最后保存的时刻, user.sync 同步到数据库。

持久化就是持久化,本身不是业务逻辑的一部分(用户才不关心,甚至上层逻辑也不关心你将数据保存在msyql还是文件里,也不关心你是否做了分库分表),因此

  1. 尽量的集中,对于整个User数据(包括n个收货地址和某种类型的联系方式)

    • 执行的时间集中
    • 代码的位置集中
  2. 不要干预业务逻辑的处理过程,比如回显的时候不用从数据库获取。

面向功能的组件化

阿里玄难:面向不确定性的软件设计几点思考 是一篇读多少遍都不过分的文章,其中就提到“面向功能的组件化设计到面向业务的对象化设计”。controller-service-dao 中包含大量的service,也是面向功能的组件化设计,“因此按抽象归纳,组件化设计的软件系统,随着业务发展,补丁越来越多,运行几年就会被推倒重来是它的宿命”

大量的service 有几个问题

  1. 多了之后,经常出现互相引用的情况。因为按领域划分的话,一定是大概念调用(多个)小概念,从上到下发散式的调用。而对于面向功能的组件化设计,以班级-学生为例,ClassService 可能要获取班级内所有学生姓名的接口, StudentService 有获取班级 班主任老师姓名的需求,必定会彼此相互依赖。
  2. 以京东业务类,既有自营也有第三方店铺,既有京东配送也有第三方配送。假设有一个订单服务,按传统设计会有OrderService,其尴尬之处是 自营非自营的订单对其来说都是一个Order对象,当然会有一个类似type的字段来标记其是否自营订单。但因为自营非自营订单的处理逻辑不同, 这时if else就不可避免了。在这个例子中,“面向功能的组件化” 对多态的表达能力不足,对能力的复用是服务化 而不是 “继承”(面向对象理念在架构设计上的延伸)的方式。“面向业务的对象化设计” 则会有Order、自营Order、第三方Order 等对象。称呼、行为 与代码的实际表现是一致的,阿里玄难:面向不确定性的软件设计几点思考 甚至提到 阿里以后真的会有一个类 叫天猫、淘宝等。

领域知识的丢失

你或许以为你不需要领域驱动设计我们或许很容易就能设想到一个毫无规划设计的城市,纵横交错的路网、杂乱无章式的建筑布局、各种凌乱的棚户区设计,恰好象征着软件设计的无序性,也恰好体现了软件企业在经费不足、组织缺乏管理、开发者能力不足、软件随时随地想改就改时的行业现状,只能说这样的软件是最能符合当时实际劳动生产力水平的产品。

程序员们掌控系统的方式,就是靠数据库建模来驱动软件开发的古老模式,而且几乎都是面向过程式的代码,这些代码的流程几乎一模一样,只需简单的按照步骤,一步步套模式,轻易就能学会。

  1. 查看用户界面,定义需要绑定到界面的模型和层级结构。
  2. 设计数据库,不管什么类型的项目,先根据客户提供的业务表单、将其转化成实体关系(ER图)、然后建立对应的代码模型。有可能使用专业软件设计ER图,也有可能会使用Navicat软件设计ER图。
  3. 设计接口,然后把数据拼凑成用户界面层所需的对象。
  4. 代码层次结构为传统的三层架构,严格按照用户界面层、业务逻辑层、数据访问层进行设计,有时候会引入依赖注入框架,实现不同层次间的解耦。但是有时候程序员不会严格区分需要编写的代码,究竟是属于哪个层次应该囊括的内容。

三层架构的问题:

  1. 与用户行为相关的操作割裂的存放在不同层。有的可能放在用户界面层、有的可能放在数据访问层、有的可能放在业务逻辑层,造成了领域知识的丢失
  2. 用户界面层使用接口作为外观或者一种行为、开发者会使用自己独立的风格习惯来定义这种行为,就容易造成术语和规则不统一,也会为后期产品的维护迭代造成问题。PS:也就是同一个业务系统,可能因为ui界面设计不同就导致 代码上差别很大

正如“罗马不是一天建成的”,屎山也同样如此。这样的写法在代码刚刚编写之初并没有问题,只是随着业务变化、时间的积累、程序员的水平、方法重构、新技术新组件的引入,代码将成为屎山

毋庸置疑,数据库建模驱动软件开发具有速度快、学习成本低的显著特点,在许多项目中,能在短期内可以给开发者带来许多便利;而应用领域驱动设计,则可以在更长的维护周期内,给软件维护带来实质性好处

从整体组织的角度看待技术债,避免技术破产对软件进行合理的变更需要花费不合理的时间来实现,技术债是沟通不畅的三阶效应。这是缺乏适当抽象的症状,而这反过来又源于对问题领域建模的不足。这意味着没有进行充分的沟通。

  1. 软件肮脏的秘密在于,我们可以对我们无法清晰表达的问题实施解决方案。如果我们的软件是“错误的”,那么正确的行为总是只需一个 if 分支。通过使用 if 分支来补偿我们糟糕的领域模型。
  2. 只要我们缺乏正确的概念,我们的思维以及我们与他人的交流就会变得笨拙而迂回。想象一下,在不知道狗(dog)这个单词或者甚至不知道动物(animal)这个单词的情况下,试图给某人讲一个关于狗的故事。“它是一种急切的、摇尾巴的、有四条腿的生物”。这听起来很傻,但我在项目中多次遇到这种情况。试图“修复”没有正确概念的代码很可能会失败,因为错误的概念没有优雅或干净的组织。
  3. 我们所说的技术债实际上是源于业务领域建模的不足,并且最终是由沟通和协作问题引起的,那么这不是开发人员可以自行解决的问题。事实上,认为开发人员能够并且应该单独处理技术债是导致技术债的另种思维症状。对于开发人员来说,认为他们所需要的只是一点时间来把事情做好就更舒服了,是错误的。

碎碎念

只有架构分层是不够的,还需要更详细的逻辑分层,DDD领域驱动设计正是一个详细帮助建立丰富的有行为的领域模型的方法学。

数据驱动SQL —->服务驱动SOA —–>领域驱动

聚合 >松耦合>重用 ==> 事件驱动>依赖注入>继承

过去系统分析和系统设计都是分离的,这样割裂的结果导致,需求分析的结果无法直接进行设计编程,而能够进行编程运行的代码却扭曲需求,导致客户运行软件后才发现很多功能不是自己想要的,而且软件不能快速跟随需求变化。

DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。重点不同导致编程世界观不同。