极客时间已完结课程限时免费阅读

20 | 领域驱动设计:35岁的程序员应该写什么样的代码?

20 | 领域驱动设计:35岁的程序员应该写什么样的代码?-极客时间

20 | 领域驱动设计:35岁的程序员应该写什么样的代码?

讲述:李智慧

时长12:28大小10.00M

我在阿里巴巴工作的头一年,坐在我对面的同事负责开发一个公司统一的运维系统。他对这个系统经过谨慎的调研和认真的思考,花费了半年多的时间开发,终于开发完了。然后邀请各个部门的相关同事做发布评审,如果大家没什么意见就发布上线,全公司范围统一推广使用。
结果在这个发布会上,几乎所有部门的同事都提出了不同的意见:虽然这个功能是我们需要的,但是那个特性却是不能接受的,我们以往不是这样的……
最糟糕的是,不同部门的这个功能和那个特性又几乎不相同。最终讨论的结果是,这个系统不发布推广,需要重新设计。
这个同事又花了几个月的时间尝试满足所有部门的不同的需求,最终发现无法统一这些功能需求,于是辞职了……
他离职后,有次会上我们又讨论起这个项目为什么会失败,其中有个同事的话让我印象深刻,他的话的大意是:如果你对自己要开发的业务领域没有清晰的定义和边界,没有设计系统的领域模型,而仅仅跟着所谓的需求不断开发功能,一旦需求来自多个方面,就可能发生需求冲突,或者随着时间的推移,前后功能也会发生冲突,这时你越是试图弥补这些冲突,就越是陷入更大的冲突之中。
回想一下我经历的各种项目,似乎确实如此。用户或者产品经理的需求零零散散,不断变更。工程师在各处代码中寻找可以实现这些需求变更的代码,修修补补。软件只有需求分析,并没有真正的设计,系统没有一个统一的领域模型维持其内在的逻辑一致性。功能特性并不是按照领域模型内在的逻辑设计,而是按照各色人等自己的主观想象设计。项目时间一长,各种困难重重,需求不断延期,线上 bug 不断,管理者考虑是不是要推到重来,而程序员则考虑是不是要跑路。

领域模型模式

目前企业级应用开发中,业务逻辑的组织方式主要是事务脚本模式。事务脚本按照业务处理的过程组织业务逻辑,每个过程处理来自客户端的单个请求。客户端的每次请求都包含了一定的业务处理逻辑,而程序则按照每次请求的业务逻辑进行划分。
事务脚本模式典型的就是 Controller→Service→Dao 这样的程序设计模式。Controller 封装用户请求,根据请求参数构造一些数据对象调用 Service,Service 里面包含大量的业务逻辑代码,完成对数据的处理,期间可能需要通过 Dao 从数据库中获取数据,或者将数据写入数据库中。
比如这样一个业务场景:每个销售合同都包含一个产品,根据销售的不同产品类型计算收入,当用户支付的时候,需要计算合同收入。
按照事务脚本模式,也就是我们目前习惯的方法,程序设计可能是这样的:
用户发起请求到 Controller,Controller 调用 Service 的 calculateRecognition 方法,并将合同 ID 传递过去计算确认收入。Service 根据合同 ID 调用 Dao 查找合同信息,根据合同获得产品类型,再根据产品类型计算收入,然后把确认收入保存到数据库。
这里一个很大的问题在于,不同产品类型收入的计算方法不同,如果修改计算方法,或者增加新的产品类型,都需要修改这个 Service 类,随着业务不断复杂,这个类会变得越来越难以维护。
在这里,Service 只是用来放收入计算方法的一个类,并没有什么设计的约束。如果有一天,另一个客户端需要计算另一种产品类型收入,很大可能会重新写一个 Service。于是,相同的业务在不同的地方维护,事情变得更加复杂。
由于事务脚本模式中,Service、Dao 这些对象只有方法,没有数值成员变量,而方法调用时传递的数值对象没有方法(或者只有一些 getter、setter 方法),因此事务脚本又被称作贫血模型
领域模型模式和事务脚本模式不同。在领域模型模式下,业务逻辑围绕领域模型设计。比如收入确认是和合同强相关的,是合同对象的一个职责,那么合同对象就应该提供一个 calculateRecognition 方法计算收入。
领域模型中的对象和事务脚本中的对象有很大的不同,比如事务脚本中也有合同 Contract 这个对象,但是这个 Contract 只包含合同的数据信息,不包含和合同有关的计算逻辑,计算逻辑在 Service 类里。
而领域模型的对象则包含了对象的数据和计算逻辑,比如合同对象,既包含合同数据,也包含合同相关的计算。因此从面向对象的角度看,领域模型才是真正的面向对象。如果用领域模型设计上面的合同收入确认,是这样的:
计算收入的请求直接提交给合同对象 Contract,这个时候,就无需传递合同 ID,因为请求的合同对象就是这个 ID 的对象。合同对象聚合了一个产品对 Product,并调用这个 product 的 calculateRecognition 方法,把合同对象传递过去。不同产品关联不同的收入确认策略 recognitionStrategy,调用 recognitionStrategy 的 calculateRecognition,完成收入对象 revenueRecognition 的创建,也就完成了收入计算。
这里 Contract 和 Product 都是领域模型对象,领域模型是合并了行为和数据的领域的对象模型。通过领域模型对象的交互完成业务逻辑的实现,也就是说,设计好了领域模型对象,也就设计好了业务逻辑实现。和事务脚本被称作贫血模型相对应的,领域模型也被称为充血模型。
对于复杂的业务逻辑实现来说,用领域模型模式更有优势。特别是在持续的需求变更和业务迭代过程中,把握好领域模型,对业务逻辑本身也会有更清晰的认识。使用领域模型增加新的产品类型的时候,就不需要修改现有的代码,只需要扩展新的产品类和收入策略类就可以了。
在需求变更过程中,如果一个需求和领域模型有冲突,和模型的定义以及模型间的交互逻辑不一致,那么很有可能这个需求本身就是伪需求。很多看似合理的需求其实和业务的内在逻辑是有冲突的,这样的需求也不会带来业务的价值,通过领域模型分析,可以识别出这样的伪需求,使系统更好地保持一致性,也可以使开发资源投入到更有价值的地方去。

领域驱动设计(DDD)

前面我讲到领域模型模式,那么如何用领域模型模式设计一个完整而复杂的系统,有没有完整的方法和过程指导整个系统的设计?领域驱动设计,即 DDD 就是用来解决这一问题的。
领域是一个组织所做的事情以及其包含的一切,通俗地说,就是组织的业务范围和做事方式,也是软件开发的目标范围。比如对于淘宝这样一个以电子商务为主要业务的组织,C2C 电子商务就是它的领域。领域驱动设计就是从领域出发,分析领域内模型及其关系,进而设计软件系统的方法。
但是如果我们说要对 C2C 电子商务这个领域进行建模设计,那么这个范围就太大了,不知道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、库存、物流、发票等。强相关的多个子域组成一个限界上下文,限界上下文是对业务领域范围的描述,对于系统实现而言,可以想象成相当于是一个子系统或者是一个模块。限界上下文和子域共同组成组织的领域,如下:
图片源自网络
不同的限界上下文,也就是不同的子系统或者模块之间会有各种的交互合作。如何设计这些交互合作呢?DDD 使用上下文映射图来完成,如下:
在 DDD 中,领域模型对象也被称为实体,每个实体都是唯一的,具有一个唯一标识,一个订单对象是一个实体,一个产品对象也是一个实体,订单 ID 或者产品 ID 是它们的唯一标识。实体可能会发生变化,比如订单的状态会变化,但是它们的唯一标识不会变化。
实体设计是 DDD 的核心所在,首先通过业务分析,识别出实体对象,然后通过相关的业务逻辑设计实体的属性和方法。这里最重要的,是要把握住实体的特征是什么,实体应该承担什么职责,不应该承担什么职责,分析的时候要放在业务场景和限界上下文中,而不是想当然地认为这样的实体就应该承担这样的角色。
事实上,并不是领域内的对象都应该被设计为实体,DDD 推荐尽可能将对象设计为值对象。比如像住址这样的对象就是典型的值对象,也许建在住址上的房子可以被当做一个实体,但是住址仅仅是对房子的一个描述,像这样仅仅用来做度量或描述的对象应该被设计为值对象。
值对象的一个特点是不变性,一个值对象创建以后就不能再改变了。如果地址改变了,那就是一个新地址,而一个订单实体则可能会经历创建、待支付、已支付、代发货、已发货、待签收、待评价等各种变化。
领域实体和限界上下文包含了业务的主要逻辑,但是最终如何构建一个系统,如何将领域实体对外暴露,开发出一个完整的系统。事实上,DDD 支持各种架构方案,比如典型的分层架构
领域实体被放置在领域层,通过应用层对领域实体进行包装,最终提供一组访问接口,通过接口层对外开放。
六边形架构是 DDD 中比较知名的一种架构方式,领域模型通过应用程序封装成一个相对比较独立的模块,而不同的外部系统则通过不同的适配器和领域模型交互,比如可以通过 HTTP 接口访问领域模型,也可以通过 Web Service 或者消息队列访问领域模型,只需要为这些不同的访问接口提供不同的适配器就可以了。
领域驱动设计的技术体系内还有其他一些方法和概念,但是最核心的还是领域模型本身,通过领域实体及其交互完成业务逻辑处理才是 DDD 的核心目标。至于是不是用了 CQRS,是不是事件驱动,有没有事件溯源,并不是 DDD 的核心。

小结

回到我们的题目,一个 35 岁的程序员应该写什么样的代码?如果一个工作十多年的程序员,还是仅仅写一些跟他工作第一年差不多的 CRUD 代码。那么他迟早会遇到自己的职业危机。公司必然愿意用更年轻、更努力,当然也更低薪水的程序员来代替他。至于学习新技术的能力,其实多年工作经验也并没有太多帮助,有时候也许还是劣势。
在我看来,35 岁的程序员真正有优势的是他在一个业务领域的多年积淀,对业务领域有更深刻的理解和认知。
那么如何将这些业务沉淀和理解反映到工作中,体现在代码中呢?也许可以尝试探索领域驱动设计。如果一个人有多年的领域经验,那么必然对领域模型设计有更深刻的认识,把握好领域模型在不断的需求变更中的演进,使系统维持更好的活力,并因此体现自己真正的价值。

思考题

你觉得大龄程序员的优势是什么?如何在公司保持自己的优势和地位?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 13

提建议

上一篇
19 | 组件设计原则:组件的边界在哪里?
下一篇
答疑丨对于设计模式而言,场景到底有多重要?
unpreview
 写留言

精选留言(18)

  • leon
    2020-01-11
    公司是不需要程序员的,公司只需要能创造价值的人。大龄程序员的优势是他不仅仅是一个程序员了,他更是在某一业务、某一领域内的领域专家,而且知道如何用技术去解决实际问题,创造价值。
    51
  • 搞怪者😘 😒 �...
    2020-01-06
    老师可以提供一下领域模型的例子吗,光看这个有点不懂
    共 1 条评论
    19
  • Zend
    2020-01-08
    记得在老师的一篇文章中说好的程序员的工作效率是一般程序员的10倍甚至更多。好的程序员不仅代码写的漂亮,可维护性强 ,面对需求变动时能更从容面对。那如何做一个好的程序员 我想对领域的理解,对需求的理解 ,具有一定的前瞻性思考,适时提出自己的建设性建议,通过代码让公司的业务系统可持续发展,推动公司业务的增长。

    作者回复: 👍

    共 2 条评论
    18
  • Paul Shan
    2020-01-16
    事务脚本模式的特点在于状态都在数据库里,业务逻辑在service层,业务逻辑与状态完全分离。 领域对象模型,特点是聚合状态和操作,提供相对独立的模块和类。领域的模型的对象之一是实体,有唯一标识,实体被销毁前标识不变,能通过标识获得,实体的状态会变化。领域的另外一种对象是值对象,状态在生命周期内不变,因而更简单。领域对象因为有状态可以表达更为丰富的关系。但是设计好的领域对象依靠的主要不是技术而是对业务的理解。大龄程序员选择的业务领域的余地还是越来越小的,可能人老了就是这样。
    展开

    作者回复: 👍

    共 2 条评论
    15
  • escray
    2020-09-25
    算是领域驱动设计的入门篇吧,如果想要深入学习,估计还得去隔壁的专栏看看,另外更重要的是如何在项目中落地。 有一个疑惑,关于实体,在面向对象分析的时候,似乎也是强调要找到业务领域、找到实体,当然可能不像领域驱动设计这么有针对性。 大龄程序员有什么优势? 先说劣势,比年轻人更贵,但是对工作的投入程度可能不如年轻人,如果不持续学习的话,那么技术也可能落伍,综合起来就是性价比太低。 优势呢,可能比年轻人成熟稳定一些,一般情况下不敢删库跑路,毕竟还要养家糊口。如果单写 CRUD 估计写不过年轻小伙子,但是如果是复杂一点的业务逻辑,经验可能还是会有一些作用。 对于大龄程序员在业务领域的积累,我有一点怀疑。其实这个和学习新技术一样,如果你不去学习和思考,那么就不会有什么积累。 最后,作为一个不必面对“35岁问题”的大龄程序员(因为我已经 40+ 了),仍然希望自己能去做技术,可是“留给中国队的时间不多了”。
    展开
    共 1 条评论
    11
  • 山猫
    2020-01-08
    第一次听到DDD这个东西还是在一年前,仔细了解后发现,其实这个早在学习软件工程的时候就被提及。简单地说,就是针对某个领域,从功能或者组件的角度去进行对象的设计。 另外关于 35 岁的问题,我认为除了要保持学习能力外,还需要有系统架构能力和丰富的经验,同时还需要了解经济、历史、政治,锻炼自己的社交和口才。
    展开

    作者回复: 👍

    10
  • 左耳朵狮子
    2020-05-30
    大龄人的优势,会学习和学习方向的效率是没有经验的人无法比的。 (比如,有经验的用google search 较没经验的用google search,可能就是两种完全不同的工具)。或者通俗来讲,学习某一方面,你不知道你不知道什么。有经验的,打一枪可能就学会了。没经验的打10枪才学会。(有人可能说,年轻的是个快枪手,那35岁怎么定的界限?心老了,18岁就已经是81岁了。这里不杠,34.5岁就年轻了?36.2岁就年老了?)
    共 1 条评论
    5
  • ant
    2020-01-06
    这一章,真的是感受到了理解层面的难度,或许当我对某一领域有了完全充分的理解,并参与了类似的模型设计,才能更好的理解理论层面!
    共 2 条评论
    3
  • 2020-08-04
    个人从传统mvc转到领域驱动的主要动力是,领域驱动的代码模型中使用设计模式提高对需求的响应能力很方便,比如校验做成责任链,加个策略实现,模板。

    作者回复: 可以写一篇文章详细讲讲你的领域驱动设计实践吗?文章发到评论区一起学习下。

    2
  • 而立斋
    2020-06-03
    最大的优势就是耐操
    2
  • 探索无止境
    2020-03-05
    老师能否加餐再通过一个案例讲讲DDD?老师总能把复杂的东西讲得清晰明了
    1
  • 靠人品去赢
    2020-01-15
    其实这个贫血模型,应该指的是Dao一类的吧,就是一个存值取值的一个东西,没啥逻辑。最后都是交给service处理,不关心业务逻辑,扩展会导致增加service,万一要做什么新的扩展关于dao导致别的service也要考虑。 这个Service只管逻辑的也是贫血模型吗?充血模型有什么比较好的最佳实践吗?
    1
  • 唐二毛
    2020-01-06
    老师,我在DDD 的实践过程中,遇到两个问题: 1. 业务逻辑包含在Aggregate A中,而业务逻辑往往需要查询多个其他aggregate , 但是 Aggregate 又不能用 @Autowired 的方式来注入service/repository, 所以我的做法是首先将依赖的aggregate 查询出来,再作为参数传入到处理业务的aggtegate A中,感觉这种做法并不是很好,因为查询aggregate B 的参数也是业务逻辑的一部分,所以无法真正将业务逻辑收在 Aggregate A 里面, 希望老师指点一下! 2. 我们的aggregate 关联链条很长,A -> B-> ... -> G , 并且,业务逻辑中往往需要用到这些关联的数据,如果用@ManyToMany的方式,就会使得 Aggregate 特别大,查询效率也很慢,还有就是循环依赖的问题,以前的做法都是用一个中间关联表来记录,但是这样好像是面向表编程,按照DDD,应该怎么设计呢? 如何在Aggregate 中来实现这些业务逻辑呢?
    展开
    共 5 条评论
    1
  • java小霸王
    2022-06-30
    优势应该是 深度和广度的积累,学习新技术的速度,看问题的本质,在某个领域的权威,同时具有一定的创造性
    1
  • 黄老板
    2021-06-22
    老师下面这段话说的很有道理,但是实际开发过程中就是存在各种伪需求客户要求实现,领导、产品不管什么设计模型不管合理性,就是霸王硬上弓就是要开发就算各种写死把代码弄的一团糟也要快速实现,这是很多中小企业目前为了生成而面临的现状,完全就是客户说啥就是啥,所以做开发越来越累了,各种无脑加班 上文原话如下: 在需求变更过程中,如果一个需求和领域模型有冲突,和模型的定义以及模型间的交互逻辑不一致,那么很有可能这个需求本身就是伪需求。很多看似合理的需求其实和业务的内在逻辑是有冲突的,这样的需求也不会带来业务的价值,通过领域模型分析,可以识别出这样的伪需求,使系统更好地保持一致性,也可以使开发资源投入到更有价值的地方去。
    展开
  • 小高
    2020-05-19
    在工作中也要努力提升作为一个程序员的编程技能和软实力。
  • X.L.
    2020-03-23
    如果能做一个项目从需求到拆解领域模型,到伪代码开发,并且有例子规范各个子域的互相调用方式就更好了
  • Haan
    2020-01-14
    大龄程序员 除了基本的技能之外,需要扎根于某一特定的领域。right?