技术

LLamaIndex入门 LLM工作流编排 Python虚拟机 LLM工作流编排 Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论及实践 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 LLM部分技术源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习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内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 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快速入门

架构

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

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

ddd反模式——CRUD的败笔

2018年11月13日

简介

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

很多项目的实际情况

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

CRUD/controller-service-dao的败笔

在日常开发中,开发主要涉及到与前端交互、业务逻辑处理和数据存储,这样就可以分成三层:「视图层」、「业务逻辑层」和「数据访问层」。MVC 架构迫使我们是面向视图来开发的,我们知道视图的变化最是不可控的,越是偏向于用户的东西,越是容易受到用户主观的影响。我们知道复杂系统必然存在的纷繁复杂的依赖,依赖不可能存在于视图部分,肯定会表现为接口的依赖。对于复杂系统,我们要强迫我们转换思维,强迫我们面向接口进行设计。结合着业务系统的复杂性,如果想要系统未来具有长期价值,不得不把大的系统进行拆分,用统一的业务语言进行描述,把不可识别的问题,拆分成可识别的问题域进行解决,这也就是现在又逐渐盛行起来的领域驱动设计的方法。领域驱动设计,强迫我们不再用数据进行驱动,而是使用领域进行驱动。遇到问题,我们先进性领域上的划分和拆解。这个问题到底属于哪个问题域,或者需要拆解到哪些问题域,然后再通过领域的组合、依赖完成最终问题的解决。

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. 判断是否你的程序只为一个业务模式服务。比如拼团业务要用到、国际业务要用到、健康业务要用到。如果是,就要提前考虑好业务身份的判断且抽象共享服务。

基于“贫血”模型的传统开发模式是否违背 OOP

  1. “贫血”模型将数据与操作分离,破坏了面向对象编程的封装特性,属于典型的面向过程编程风格。
  2. 做好领域驱动设计的关键是对业务的熟悉程度,而并不是对领域驱动设计这个概念本身的理解程度。即便我们非常清楚领域驱动设计这个概念,但是,如果我们对业务不熟悉,那么也不能得到合理的领域设计。
  3. 基于“充血”模型的 DDD 开发模式实现的代码一般是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据访问,Service 层负责业务逻辑。它与基于“贫血”模型的传统开发模式的主要区别在 Service 层
    1. 在基于“贫血”模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 类是“贫血”模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。
    2. 在基于“充血”模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 类相当于“贫血”模型中的 BO 类。与 BO 类的区别在于,Domain 类是基于“充血”模型开发的,既包含数据,又包含业务逻辑。而 Service 类变得非常“单薄”。 总结一下,基于“贫血”模型的传统的开发模式,重 Service 类,轻 BO 类;基于“充血”模型的 DDD 开发模式, 轻 Service 类,重 Domain 类。service层方法一定要有业务概念,提取、显式化用户、作者的意图,比如AService.BusinessFunc ==> BService.CrudFunc ==> ADao.CrudFunc,则业务意图在BService.CrudFunc 就丢失了,可能会被其他伙伴随意引用,BService.CrudFunc中的代码就不敢乱动,也无法根据意图做优化。一个良好拆分的系统,体现业务意图的方法不会有很多个。
  4. DDD和充血模型没有必然关系,并不是DDD才引入了充血模型。DDD首先是一个思想(战略),强调面向业务领域建模与统一语言,而非首先关注技术实现。有了业务模型之后才有落地(战术),而落地的方式有很多,与是否使用MVC,是否一定要充血模型都不必然,事实上,较适合的应该是菱形架构,过分强调充血模型也常常会带来落地的复杂度(性能、依赖、各种纠结)。

大佬总结的

我一直秉持的态度就是『知其然,知其所以然』。如果我们一直不知道为什么要这么做,那很有可能在模仿的时候会变形。

  • dao: 数据通道
  • service: 提供业务能力 controller, service , dao通过 对于数据通道的操作来实现业务逻辑的,简单来说就是通过sql的组合,来实现业务逻辑的。假设我们有三个 service (serviceA、serviceB、serviceC) 有三个dao (daoA、daoB、daoC) 他们目前是意义对应的,很美好是吧。但是当业务逻辑开始演进的时候,慢慢的问题就开始出现了。比如serviceA 需要用到daoB的一部分数据(假设是1-1的场景),于是我们开始向serviceA中加入daoB。有一天产品形态衍生出反向关联的场景的时候,我们又会往serviceB里加入daoA,这就出现了 serviceA和serviceB 中同时出现了 daoA和daoB ,当一个新人需要改动这段代码的时候他往往会有一个疑惑我到底把daoA相关的业务逻辑写在serviceA中还是serviceB中呢?这时候serviceC 出现了,说我希望复用一段业务serviceA中的业务逻辑,于是serviceC中出现了serviceA,产品这时候又提出了需求说,我希望serviceC对应的业务逻辑能显示daoA对应数据的一部分数据,之后serviceC中又出现了daoA 。现在如果是一个完全不知道演进的人已经完全看不懂这个依赖了。这有解法吗?有 在软件工程中永远能通过加一个中间层去解决问题,于是在阿里巴巴的java 开发手册中有了manager 的角色,目的是聚合service 让serviceA和serviceB的逻辑通过manager聚合而不是通过dao共享的方式去解决,但是实现随着演进仍然是会出现上面的问题的,只是把时间拖后的而已。这里个人有一个粗浅的认知就是分层结构的目的的理想状态是一个层次分明的树状结构,如果出现了网状结构的时候,去靠分层来避免unknown,将会变得困难,简单来说分层结构不停迭代一定会遇到的问题:1.无法梳理依赖(依赖混乱) 2.职责模糊(新方法不知道加哪里)

单纯的层次模型在飞速迭代的今天在抗unknow的能力不是很强,那还有办法来减缓这种unknow的发生呢?我们来剖析一下分层模型的意图,分层模型通过操作sql来组合数据,将职责分为两个部分,即我上面提到的操作sql和sql入口。发现了Unknown的源头的业务的多变导致的unknown,那我们能不能换个角度去划分职责,将变化的东西更集中起来呢?大胆的提出一个想法把业务逻辑和操作sql的逻辑分开是不是一个好想法?再大胆一点,在面向对象编程的今天,我们能不能利用好面向对象的思想去解决这个问题呢?我们发现,在传统的service + dao 解决方案中所有的所有编程方式的其实都是面向流程编程(个人认为面向流程编程也是非常优秀的解决方案)。但是面向流程对工程师的要求非常高(模块化,组件化的)对工程师要求非常了解业务,需要合理将一个或者几个流程编程一个可执行的函数,不然代码就会陷入unknown状态。所以面向对象编程该怎么解这个问题呢?利用一个对象的天然特性 1-N。也就是对象天然是有层次的,如果我们将业务逻辑包装在对象的方法中,靠父对象去操作子对象的业务逻辑,是不是完全不会混乱了? 等等那层次模型也可以做啊,为什么不选用service 套service 的方案呢?等等是不是发现serviceA依赖serviceB,serviceB依赖serviceC,serviceC又依赖了serviceA?开始循环依赖了,又开始有点unknown的味道了?所以我们斗胆提出一个假设,有一个对象是不可以被其他对象依赖,这样就可以靠对象的从属关系去解service 的unknown 了。巧了,在ddd中这种对象叫做聚合根,有了他我们就有严格的层次模型了。现在我们演进出的方案是一个service 套用一个service 并且有个service 是不能被依赖的,解决了依赖复杂的问题,那还有一个问题:方法要写哪里?当我们发现我们称呼一个人为总管的时候,这个人给人的形象就是无所不能的,但是当我们把一个人称为一个财务,或者设计的时候,我们就已经知道他什么事情能干什么事情不能干了,所以当我们把StudentService 改名为 Student 这个时候我们就知道,我们就知道显示考试成绩的方法应该放在 Student里面的ExamRecord里面,如果再演进出一个获取平均分的方法我们也自然知道不应该直接加在student里面而应该ExamRecord里面。再回头一想,这不就是面相对象的设计方式吗?这就是ddd中的另外一个思想,弱化service 换言之就是,把业务逻辑写到业务对象里面。还有很多想法是可以从三层的劣势推到出来的包括事件驱动,限界上下文等等的。

一个收音机你看知道知道是一个收音机,知道xx按钮大概是调节音量的,但是如果把收音机的外壳拆了,给你看电路板,你还认识嘛?有没有想过为什么同一个东西,在不同的形态下,大家会对同一个东西有不同的感受呢?其实仅仅是封装是一个合理的封装而已。当我们接到制作收音机的需求时,根据传统的三层模型(controller/handler&service&DAO),我们实现的第一步可能会有以下几种情况:

  1. 从壳体设计(接口设计)开始;
  2. 从元器件设计(数据库设计)开始;
  3. 从布线设计(服务设计)开始。 然而,每种设计的出发点都会带来一些问题。接下来,我们将分别演绎这三种设计可能会遇到的问题。
  4. 从壳开始设计(从接口开始设计),根据需求,按钮能调节大小音量,按钮能调节无线电频率,但不知道不知道我的最后的大小,最后按钮的排布。在我们代码开发的情况下,就是发现入参不够,或者返回值不能表达所有的情况。
  5. 从元件开始设计(从数据库设计),当从元件开始设计的时候看上去好像挺美好的,因为关注的细节非常多,但是就是由于关注的细节特别多,被忽视的也会特别多,比如搞了两个非常完美的元件,但是他们的电压不同或者是电流不同,于是我就加了个电阻进来,发现这个电阻放进来之后会导致其他地方的电流电压不对,然后又花了非常大的力气调节。在我们的代码开发的情况这个就是发现我们在这个表里面少了一个字段,或者又少了个 map 表等等的。
  6. 从布线开始设计(从service设计)/ Start designing from wiring (from service design),当首先从布线设计的角度出发,大家会发现另外一个问题,具体那个元件通了哪几根线,那几个是公用的,那几个是独立的好混乱啊,因为元器件太多了,可能加一个电阻或者扣一个电阻,会对已经设计好的布线有极大的影响。在我们的代码开发的时候,其实就是数据库的字段多了一个或者少了一个,大家会被搞的毫无头绪。

从接口设计的关注点着重行为,不关注实体之间的关联关系,所以一旦设计到关联关系,就很容易忽视。从数据库设计的关注点是数据和关联关系,但不关注到底有多少个业务对象,有多少种业务行为。从 service 设计其实规避了从接口和从数据库设计所带来的问题,看上去没什么大问题,还是不可避免的提到我上面说的混乱情况呢?没有模块化。事实上在自动化这种偏爱模块化设计的行业内,他们第一步是定义模块,和模块和模块之间的交互。而不是直接开始设计线路,收音机的模块划分如下图。所以他们的第一步确实是在设计线路但不完全是在设计线路。那为什么我们写 service 的时候那么会那么容易出问题的一部分原因找到了,应该从拆分模块开始,然后才是service的设计。由于我们错误的开始,然后会导致我们遇到一系列问题,然后我们又会用一些不合理的方式解决,这些不合理的方式导致我们的service陷入一个更加混乱的状态,导致我们的代码无法维护。究竟是哪些原因导致我们代码不能维护呢?复制代码,总是有一些东西不那么容易提取到函数中。源头的源头 – 可传递的太少了:除了数据,1-N 、1-1 的依赖关系(比如一个函数两个参数,但是这两个参数也有约束关系,此时函数参数最好是一个对象,包含这两个参数);业务逻辑 没有传递,我们复制的代码大部分都是这部分的逻辑,另外实体和数据库的数据对不上,所以很多复制代码都是要不停的 query 对象来使得自己和数据库的数据保持一致(更新的时候担心实体中的字段是否是最新的值)。这个有解法吗? 有解法的,如果我们能把请求更新的所有逻辑都放在业务实体中是不是就有了一致的保证了?

为什么我们做不好收音机(服务)?总结一下,在我们利用自己的思维惯性设计代码的时候,通常会一下问题

  • 错误的开始,设计的时候不先梳理依赖和拆分(模块化)
  • 混乱的依赖导致不得不复制代码,导致代码更加混乱
  • 代码难以传递依赖和业务逻辑,导致业务逻辑不会统一写在统一个地方,导致更加混乱

如果我们尽量使用充血模型会得到什么好处呢? 业务逻辑和依赖关系都可以传递了,我们回头看看失血模型下我们的代码,是不是相当于当我们的合作伙伴要调用我们的时候,我们丢出去了一堆电子元器件并且和他说『自己去把线连起来』是不是多少有那么一点点不合理?在充血模型下我们的代码是由什么组成的呢?

  1. Entity,这里面写的是关于这个实体的业务逻辑和他的关联的对象,并且触发关联对象的业务逻辑,只要有业务逻辑优先是写在 entity 里面的
  2. ValueObject,业务实体 这是业务实体的另一种体现,和 entity 的区别就是有没有 id
  3. Repository,用来获取业务实体,或者是保存业务实体,首先声明这一定是一个接口,用来获取 entity, 和我们认知的 dao 有什么区别呢?DAO 返回的对象不一定是 entity, 但是 repository 返回的对象一定是 entity。同时因为是一个接口所以可以用来屏蔽到底是怎么取数据的(localcache, redis, mysql)。此外,repository 的接口会写在 entity 里面, 因为Entity 是不能依赖非 entity 包下的东西的,在充血模型下,实际上 repository 是一部分 entity 的,entity 禁止依赖 redis 禁止依赖一个 xxxclient。
  4. Service,entity 处理不了的逻辑,比如说从 repository 请求实体,将错误转换等等的
  5. http/rpc handler,只能做简单的数据转换 & 触发,转换成service 能处理的内部指令,交给 entity 执行
  6. Eventbus/timer handler,只能做简单的数据转换 & 触发,转换成 service 能处理的内部指令交给 entity 执行

测试阶段。如果是普通的三层模型,从数据库取数据的地方是绕不开的,直接把单测的的模式升级为非常难。贫血/充血对象都可以在内存里面构建出来,所以校验一段业务逻辑写的在内存里面构建一个对象,就可以直接校验自己的业务逻辑写的对不对了。

什么是高内聚,底耦合呢?我解释不出来,但是在众多解释高内聚低耦合的一个关键词是『模块』,模块内高内聚,模块外低耦合。那问题来了你写的代码有模块吗?如果你写的代码没有模块的概念的话,是不是就没有办法区分什么时候应该提高内聚,什么时候应该降低耦合呢?首先,模块是非常主观的意识,基本上是随着事务发展的情况下认为划定的。那问题来了,我们有什么方法能更方便的划归模块吗?一味的靠着经验积累将会大大这种效率太低了,甚至在没有正确实践的情况下,经验都没有办法积累。有没有办法比较简单的来划分所谓的模块这个概念呢?其实是有的这里可以引入ddd 的概念限界上下文。那这个限界上下文应该如何划分呢?首先是一个明确的点,能产生依赖的才有可能是一个限界上下文中。注意这里是依赖,而非关联。如何高内聚低耦合呢?

  • 同一限界上下文中,尽量使用函数依赖或者实体依赖,这样方便梳理逻辑和追踪代码,也更容易拿到聚合根这个上下文
  • 不同限界上下文中,尽量只依赖数据或者消息,消灭实体依赖,防止改动的蔓延和放大。所以我们一般一层单独的桥接层(特别注明非业务逻辑层)来将外部的数据转换为我们内部依赖的实体。

在六边形架构下我们怎么写代码呢?

  1. 先按照需求期间产品和产品讨论出来的 uml 图 一一建立好业务实体,要和 uml 图一模一样字段和行为。当发现没法实现的时候先去和相关人员讨论 uml 图上的哪个节点发生了变化
  2. 为业务实体写单测代码,这一步的 repository 是个接口,所以直接 mock 就行
  3. 完成 service ,通过对N聚合根的操作编排来组装业务逻辑
  4. 实现 repository 接口,怼到数据库/cache上就行
  5. 实现 handler,怼个适配器就行 我们是按照洋葱架构一步步由内向外写代码的,并且大部分情况通过内存对象就可以完成测试

ddd 有显著的缺点

  • 代码量大(越1.5-2倍)
  • 要求开发人员对业务有了解,不能上来就干
  • 业务逻辑和技术逻辑分开有有些需要技术逻辑参与的业务逻辑就不好搞(转账的事务)

业务层臃肿,能力层单薄

业务系统架构实践总结 最初设计时做四层划分,也比较清晰。最核心的设计点是,biz层编排“可复用的”service层,完成一个场景逻辑表达。问题在于:

  1. Service本身的划分、定位,相对随意,从第一感觉而来,并未经过领域划分这样的设计。所以这里特意叫service,从而区别于domain的表达,我采用的领域划分方法下文会阐述;
  2. Service本身不可扩展,多态业务冲击下,为适配此service能力而存在的个性向共性的转换逻辑上浮。 因为这些问题的存在随更多业务接入的架构演进下,组织扩大后的人员差异扩大下,会导致几个问题:
  3. biz层越发的膨胀,service层越发的萎缩。biz层里充斥了各种本该往下沉淀的可复用业务逻辑,service层则几乎萎缩为dao;
  4. 人员的差异下,service实例颗粒度不一(有萎缩有膨胀),有重复相似的。biz实例亦是如此;
  5. 由于biz膨胀,层内会发展为两小层,上小层为面向单一业务场景的“业务biz层”,下小层为通用可复用场景的“通用biz层”,且这两层隐约存在,随研发个体认知差异或隐或现。由于service的萎缩,biz层会干脆直接调用dao,且因人而异的随机性差异,导致biz往下的调用关系呈乱麻态。

service间网状调用

核心的问题还是对service这层的颗粒度、职责定位不清晰,对增量service的架构监管不足,业务压力下(祸起于常见的倒排需求,常以需求完整度妥协+架构方案妥协追赶deadline死线,挖下损害长期利益的坑),一线研发同学就容易凭感觉去新增service。

基于原本bizA -> serviceA 的实现链路下,随新增业务逻辑新起的serviceB(事是A的事,但不合适放serviceA,所以新建),链路演变成了bizA -> serviceA -> serviceB。这样的趋势持续发展下去,会发现bizA下的service调用链路越发的复杂,呈现为一颗深度调用树,而biz层失去了业务编排的作用退化为一个业务场景入口的标志符。一个新同学除非步履维艰的推演完成全串调用代码,否则不能知晓bizA业务场景真实完全的意义。步履维艰在于,这是一颗深度调用树,当你阅读到serviceE时,很可能你已经忘记了最初是从哪个biz入口看过来的,以及接下来你将还会遇到哪些service讲述剩余的业务逻辑。而一个写的并不那么好的代码,还常将这些service的调用关系,隐藏在极其隐晦的角落,给阅读者增加复杂。譬如通过一个叫util的类,完成了service B -> C这一主干链路的传递。一个service的改动,几乎无法评估其产生的影响面。而因为无法评估,又怕改出故障,在业务需求压力下,会出现各类fork行为加剧加速腐化。这是恶性循环。

搞得好像一切为了持久化

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

  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之于业务支撑的意义研发效能降低的重要原因在于:更多的时候,我们还是按照“业务能跑起来,怎么快怎么来“的逻辑去做相关工作,遇水搭桥,遇山开洞,然后直达目的地,进行信息的传达、数据库字段的操作。这样的过程,违背了我们”希望通过业务场景,丰富平台能力,同时保证内核干净“的初衷。能力应该是基于相对多的用例、相对完善的思考进行抽象,是横向统一看,有更深刻的理解,但是垂直的交付,让我们更加纵向地处理问题,往往只是“窥探”了链路,在交付时长和业务节点的限制下,很难想得更加全面、深刻,难以做出更通用的设计。那么,为什么关注DDD?如果说DDD直击了软件复杂度的核心——“问题域”,那可能还是比较抽象。具体来说,因为这的确符合我们追求的价值观【提升长期的生产效率】:

  1. 细分领域,培养专业的人、事:因为DDD的核心是要求让各个领域做好理解和封装,把一个业务需求拆解、安放在各自合理的地方,通过这样的分解与沉淀,保证了领域的输入,能够得到长期可持续的发展,形成竞争力。
  2. 机制保障,不依赖易变的事物:DDD其实在总结很多通用的技巧和经验,能够让这样的实施更具有确定性。无论是聚合根对领域实体的管控能力、限界上下文的交互策略、领域内核的抽象地位…等等,一旦选择尊奉,确定下来,就能够落在代码结构、组织关系、团队文档中,形成共识,不会因为人员等因素的变化而剧烈波动。

业务系统架构实践总结

个人认为核心有2个点:

  1. 起步时的domain设计:有理论支撑的domain划分和凭经验感觉划分的service,对整个架构生命力的影响是完全不同的;
  2. 过程中的架构原则:必须有几个易记牢记并坚持贯彻的架构原则,在实操层面上,对整个架构性命的维持是完全不同的。
    1. 厚domain薄biz: 从系统架构内观的角度看,必须要先有domain再有biz,domain承载了该应用最核心的业务能力,好的架构里,依domain代码逐个阅读便能鸟瞰该应用的核心能力,厚domain薄biz,domain要敦实、包容、开放;biz是面向场景的,核心是复用下面的domain搭建出一个业务场景,讲究灵活。biz是面向场景的一个动词视角,domain是面向一簇模型的名词视角,场景必发散,模型需收敛,动词表灵动,名词代沉稳,这是核心的差异。
    2. 领域设计,最核心的是划分,切的位置、切出来的大小,极大的决定了一个架构的生命长度。

碎碎念

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

数据驱动SQL ---->服务驱动SOA ----->领域驱动
聚合 >松耦合>重用 ==> 事件驱动>依赖注入>继承

过去系统分析和系统设计都是分离的,这样割裂的结果导致,需求分析的结果无法直接进行设计编程,而能够进行编程运行的代码却扭曲需求,导致客户运行软件后才发现很多功能不是自己想要的,而且软件不能快速跟随需求变化。DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。重点不同导致编程世界观不同。