文档章节

谈谈PHP系统中的领域驱动开发

ChefXu
 ChefXu
发布于 2017/09/05 10:54
字数 4166
阅读 1.3K
收藏 0

DDD虽然很火但是理论实在过于枯燥,很多人在软件开发的焦油坑中被推荐了DDD,以为终于找到一种解救自己的办法,但是却很快迷失在无数的概念中。通用语言,一种UML的升级语言吗?贫血模型,你居然说我用了10多年的模型是贫血的?领域服务,和我定义的Service有什么区别?聚合根、限定上下文、CQRS、Event Source,有必要搞这么复杂吗?于是很多人在没有理解DDD之前就放弃了——他们渴望得到一把屠龙刀能快刀斩乱麻地解决问题,然而却只得到一本晦涩难懂故弄玄虚的武功秘籍。

本文基于个人对于DDD的理解,尽量避开DDD枯燥的理论知识,希望通过本文能让读者对DDD是什么有个简单的认识。本文不能指导你去实施DDD,只能是一篇基本概念的扫盲,限于本人水平所限,对DDD理解难免有误,请抱有怀疑精神阅读。完整的DDD理论请阅读专门的DDD著作。


软件开发背景

DDD2004年就被提出来了, 为什么最近却越来越火,在国外已经成了开发标准。我觉得根本原因是,现在软件开发正变得越来越复杂。虽然随着技术的发展,一些原来实现起来困难的事情有了简单的技术解决方案,但是人类对于极致体验的需求,不断对软件设计提出挑战,于是新的问题又不断被提出来。比如,曾经在手机上播放32和弦音乐是一件技术含量很高的事情,现在几乎不需要任何技术成本就能实现,但人对于手机的需求早已经不在这个层次上了,人们对技术提出了新的要求。从某个方面讲, 技术永远落后人的需求,这是促成技术发展的原因。更快的速度,更好的体验,更贴心的功能,也正因为如此,软件变得更复杂。

开源技术的发展,极大简化了软件的开发, 很难想象如果没有开源社区, 现在开发软件会是一种什么样的体验。但是与此同时我发现, 开源社区很擅长解决通用的技术问题, 比如C10k问题、一致性问题、大数据问题、微服务部署问题等,这些技术与特定业务无关具有普遍的适用性,容易做成通用的解决方案,同时也是云计算厂商的热情所在。但是在软件的复杂度方面,我们的社区似乎很少讨论。软件功能越来越复杂,软件迭代速度越来越快,软件复杂度越来越高。我看到的一个事实是, 随着开源技术的发展, PHP程序员在大流量、高性能技术方面已经有了长足的进步,但在开发复杂业务软件方面的能力,进步并不明显。

我认为原因是缺少社区氛围:

  1. 在很多成熟公司,PHP只是作为一种数据处理层存在,给前端格式化数据用,复杂的业务逻辑都交给其他后端语言了
  2. 在创业公司中,大多数PHP项目的复杂度并不高或者在复杂度变高之前项目就已经死掉

所以, 当你在一个以PHP为主要语言的公司,且业务复杂度变得越来越高时,问题就出现了。

业务逻辑会变得越来越复杂,但是通用的开源技术对此帮助不大


事务脚本开发

我们先试图分析下软件越来越复杂的原因。我们最习惯的一种业务开发方式是: 定义各种Service封装业务逻辑,我们称为业务逻辑层。

有问题吗?在业务逻辑变得很复杂之前没有问题,甚至可以说这是一种很好的方法,但在业务复杂度变高后,可能会出现一些复杂度不受控制的问题。这种开发方式被叫做事务脚本的开发方式。

下面我们从一个产品需求的变更看这种方式的局限性。

 

在前期,我们分析产品需求,按照产品需求设计出流程, 流程被设计到各种方法里, 每个方法看起来简单自然,每个方法只做一件事情,一切都很好。

随着时间推进,流程变得复杂,我们不得不拆解这些功能,拆解的原因有很多:

  1. 为了代码重用
  2. 为了方法可读性
  3. 仅仅是个人习惯

如果复杂度停留在这一个阶段, 情况还不算糟糕, 事实上我们很多项目复杂度就停在这个阶段。

 

时间再推进,慢慢出现的一些问题:

  1. 这个功能可以用到原来流程的A,但是又有一点小区别,那我是再写一个还是加个参数处理下呢?
  2. 我之前拆解的功能B,现在看来好像有点问题,我想调整下但是会不会影响别的功能呢,如果影响是再写一个还是加判断处理呢?
  3. 这个方法就是我要的功能, 但是它有一次发短信操作,我加个判断把发短信屏蔽下吧

软件复杂度变得越来越高后,有很多因素会诱惑你加快复杂度的增加:

  1. 这个功能比较着急,但是我不确定改了会不会影响别的地方,复制改下吧
  2. 这个方法里面这些判断是做什么的,不管了,复制下吧
  3. 这个功能单元测试明明OK怎么不行呢, 我擦里面怎么多了个判断, 这个分支没覆盖到!

软件复杂度的增加不是线性的,随着时间的推移,如果缺乏有效的管理,复杂度会急剧增加

事务脚本的特点:

  1. 开发简单,过程式的代码,容易开发,但是复杂度容易失控
  2. 过程式,即使你定义了class,也不能否认它过程式的本质,过程式本来没有问题,因为大多数web流程就是过程式的:取几个数据,格式化一下,再加个缓存。
  3. 事务脚本的方法可以相互调用,无层次依赖,最终形成一种复杂的网状调用关系
  4. 业务逻辑分散在各处,被实现为各种不同的功能方法, 这些方法并不知道自己属于哪个业务
  5. 业务逻辑,存储层逻辑,外部服务调用混杂在一起

事务脚本大多数情况下是一种很好的开发方式,特别是对于以读为主写逻辑不是很复杂的应用


领域驱动开发

但是,事情总有变得复杂的时候。这个时候,我们需要有一种方法,可以很好的控制软件复杂度的增长,领域驱动开发就是这样一种方法。关于领域驱动的知识网上有很多, 简单说DDD是解决复杂中大型软件的一套行之有效方式,在国外已经成为主流。DDD认为很多原因造成软件的复杂性,我们不可能避免这些复杂性,能做的是对复杂的问题进行控制。而一个好的领域模型是控制复杂问题的关键。

DDD给程序员带来的一个最大改变是思维习惯的改变。

事务脚本开发方式,优先考虑的是数据和行为, 设计好数据库,然后按照业务流程用各种方法把数据存下来。DDD接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。重点不同导致编程世界观不同。

 

举个具体的例子:产品需求是实现一场比武。

事务脚本开发方式:

  1. 定义一个比武的方法
  2. 产品需求是两人比武,参数A和B
  3. A使出降龙十八掌, 封装一个方法
  4. B防守
  5. B进攻
  6. ....
  7. 结束

领域驱动开发方式:

  1. 这是做什么? 比武,这是一个比武的业务领域
  2. 这个领域有什么?武林高手,每个武林高手有自己的武功特点。武林高手就是领域中的实体,因为有多个武林高手,那我们可以定义一个武林高手的抽象类或者接口, 然后实现乔峰,段誉等具体子类, 每个子类有自己的武功特点。
  3. 比武是一个具体动作, 放在哪个实体都不合适,定义成领域服务。

在事务脚本开发方式中,直接开发业务流程。在DDD开发中,我们是定义一个比武模型, 模型里有各种武林高手, 他们在一个舞台上比武。随着业务逻辑发展, 新的武林高手、多人比武等新的比武方式被加进来,事务脚本开发的方式会被添加越来越多的方法,这些方法被平铺在事务脚本里,被复制粘贴,被修改扩展,最终导致复杂度不可避免的增加。在DDD模型中,我们使用建立了一些关于比武的领域模型, 各种高手被有序封装在各种特定实体里,他们在领域服务的协助下完成业务动作。添加新的武林高手或者新的比武方式,并不会对领域模型产生冲击,最终复杂度被控制在一定范围内。

DDD接触到需求第一步就是考虑领域模型,一个好的领域模型是控制复杂问题的关键。看到领域模型代码,就看到业务需求,没有翻译没有转换,保证软件真正实现“拷贝不走样”。


领域驱动设计常见错误

数据实体

数据实体在领域驱动里叫贫血模型,就是只有数据没有行为的实体。数据实体对数据对象提供一层抽象,隔离数据库和业务逻辑,在JAVA领域曾经大行其道。这种方式的问题是:

  1. 首先,实体的抽象出发点不是构建业务模型,可能仅仅是为了屏蔽数据库细节,实体的有效性是个问题
  2. 其次,由于实体没有行为,行为代码仍然被分散在事务脚本里, 没有被按照领域模型组织起来,复杂度风险仍然存在

无领域模型

代码里有实体层,有领域服务层等看似非常DDD的组件,但是实际上这些实体和领域模型没有任何关系,程序员完全不理解业务模型的存在,这只是一种新的代码组织方式,并不理解领域驱动的真正意义。由于没有模型, 代码依然被分散在各个实体或者领域服务中,也无法灵活应对需求变化。

DDDLite

代码反应了领域模型,但是这个模型是开发理解的,缺乏和领域专家持续沟通。这种情况下的领域模型正确性存在风险, 随着时间推移, 这个风险会越来越大,DDD的作用会大大降低。

领域驱动是一种设计模式

领域驱动不是一种设计模式, 是一种软件开发方法,但是在领域驱动的实现中,会需要用到很多设计模式。设计模式来源于建筑学,把一些常见的设计处理方法加以抽象、分类和命名,成了一个个设计模式。每个设计模式有特定的适用场景。领域驱动设计是一种软件分析和设计的方法,当业务模型被设计出来后, 模型的实现可能需要用到各种设计模式。

领域驱动很复杂

我觉得主要是思维的转变,尝试定义领域模型来解决问题,这需要一个思维转变过程,有时候这会很难。

我应该立刻采取领域驱动

多数情况下,领域驱动都不是我们开发的首选方式。 因为我们对于业务的理解需要时间,而且多数情况下,我们业务逻辑并不复杂。如果你对你的代码复杂度现状满意,那就暂时不用去考虑。具体可以参见DDD评分卡,一个评估你是否需要采取DDD的计分卡。

领域驱动的一些收获

  1. 软件是复杂的,而且会越来越复杂,避免软件复杂度是不可能的, 但是我们可以控制软件复杂度
  2. 软件复杂度分为事实复杂度和随机复杂度, 前者是软件事实存在的,后者是软件开发不断迭代带来的,是可以有效控制的
  3. 软件开发最重要的技能是抽象。抽象出软件中稳定相对不可变的部分重点优化和测试,把可变部分通过参数化和配置化,从而最大程度的增加软件适应性。领域模型就是软件中相对不可变部分
  4. 领域驱动并不适用所以场景,你可以把它理解为一种复杂度管理方法,如果软件复杂度不高, 使用DDD的收益不大,当然对于业务模型的思考对于软件的扩展性是有益的(可参见DDD评分卡)
  5. 准确的领域模型是DDD的关键,但是这往往不容易,不要指望一上来就发现正确的模型
  6. 通用语言不是技术语言,不是软件技术,是一种和领域专家约定的词汇表,只要你和领域专家能就领域模型无障碍沟通,就是好的通用语言。 领域专家包括产品,运营和老板等擅长此领域的人。
  7. 开发人员容易低估业务模型的价值,模型能保证实现和需求是一致的,在模型的提炼过程中,开发能对业务更清晰地认识并发现产品需求中不清晰的地方,不要指望产品能给出一个完美的需求。
  8. 文档、注释等只能辅助理解业务,一方面保持文档的有效性是一件困难且代价较大的事情;另一方面强调文档而忽略代码本身的复杂性是一种本末倒置的做法。
  9. 重构能解决一部分问题,但是如果你复杂度不被管理,下一次重构很快又会到来,而且随着业务发展,重构的代价会越来越大。
  10. 微服务的切分和领域的边界的息息相关的,这也是国外谈论微服务基本会谈到DDD的原因,国内似乎不是这样。

 

愿你的DDD之旅一切顺利!

 

© 著作权归作者所有

ChefXu
粉丝 12
博文 6
码字总数 11518
作品 0
海淀
程序员
私信 提问
加载中

评论(1)

V
Veitor
博主的PHP项目中用了DDD吗?有没有示例或者开源学习一下啊,实施起来还是挺麻烦的
TODO:Linux安装PHP MongoDB驱动

TODO:Linux安装PHP MongoDB驱动 PHP利于学习,使用广泛,主要适用于Web开发领域。 MongoDB的主要目标是在键/值存储方式(提供了高性能和高度伸缩性)以及传统的RDBMS系统(丰富的功能)架起...

OneTODO
2016/11/04
4
0
关于VO、DTO、DO、PO设计和使用?

概念回顾: 领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处 问题: 结合您的开发经验谈谈这个对象的设计和具体的使用?

666B
2015/03/25
769
1
PHP Socket 网络应用框架 - beyod

beyod: 一个高性能分布式、事件驱动、异步非阻塞php socket网络应用框架 beyod是基于Libevent/epoll/Yii2 Framework的高性能分布式、事件驱动、异步非阻塞php实现的socket网络服务开发框架。...

月影又无痕
2018/12/27
3.5K
8
由Spring应用的瑕疵谈谈DDD的概念与应用(一)

Spring 框架已经成为构建企业级 Java 应用事实上的标准了,众多的企业项目都构建在 Spring 项目及其子项目之上,特别是 Java Web 项目,很多都使用了 Spring 并且遵循着 Web、Service、Dao 这...

aoho
2019/01/09
0
0
Memcache-eAccelerator-APC-Xcache-Redis五种php缓存加速器特点

一、说说Memcached优化方案 Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提供动态、数据...

杨太化
2015/10/14
633
0

没有更多内容

加载失败,请刷新页面

加载更多

用Markdown编程之类型

类型就是约定。而现有的类型是单纬度的。用标注法编程好处就是可以多维度。 类型基础分为: 虚 实 在此之上分为: 根 寄存器级 联 内存级 外 网络级 虚:说白了就是指针或索引之类的概念。之...

dwcz
25分钟前
56
0
WPF中的StaticResource和DynamicResource有什么区别?

在WPF中使用画笔,模板和样式等资源时,可以将它们指定为StaticResources <Rectangle Fill="{StaticResource MyBrush}" /> 或者作为DynamicResource <ItemsControl ItemTemplate="{DynamicR......

javail
51分钟前
49
0
Day07继承中的面试题 答案

1. 每一个构造方法的第一条语句默认都是:super() Object类最顶层的父类。 class Zi extends Fu{ public int num = 20; public Zi(){ //super(); System.out.println("zi"); } 2.class Test......

Lao鹰
56分钟前
46
0
每天AC系列(四):四数之和

1 题目 Leetcode第18题,给定一个数组与一个target,找出数组中的四个数之和为target的不重复的所有四个数. 2 暴力 List<List<Integer>> result = new ArrayList<>();if (nums.length == 4 &......

Blueeeeeee
今天
70
0
git clone --mirror和git clone --bare有什么区别

git clone帮助页面上有关于--mirror : 设置远程存储库的镜像。 这意味着--bare 。 但没有详细介绍--mirror克隆与--bare克隆--mirror不同。 #1楼 克隆将从远程服务器复制参考,并将其填充到名...

技术盛宴
今天
86
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部