BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

大型网站复杂业务持续重构之道——全程领域建模实践

| 作者 孙兵 关注 0 他的粉丝 发布于 2012年3月28日. 估计阅读时间: 10 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

人物介绍:

Jack Chen ——“宠物商店”的首席架构架构师,拥有丰富的软件设计与建模经验,但对新生事物持怀疑态度。

王总——“宠物商店”的总经理,从美国留学后回国创立“宠物商店”网站。一路来唾手可得的成功让他养成了固执专横的行事作风。

Spark —— Jack Chen的大学同学,一家商业软件公司的高级咨询顾问。最近热衷于宣扬“领域驱动设计”的最佳实践。

引子

就象大家所听说过的那些神奇小子创业故事一样,几只从大西洋游回的海龟找到了一个伟大的idea——在互联网上开办在线商店销售宠物。幸亏的是他们找到了投资者而且发展的很不错。但是随着时间的推移,当初“完美”的技术架构随着越来越多的装进篮子的需求后变得不堪重负。作为公司首席架构师的Jack Chen已经被这几个月“鸡毛蒜皮”的需求折磨失眠好几天啦。

Jack Chen周一一早就被兴奋的王总给喊进了办公室,立即就被王总扔出来的idea吓傻了。

“我有一个很cool的想法,我们可以在线为宠物医院提供在线预约的服务业务。而不仅仅是卖掉它们,你知道这意味着什么吗?这是一个年产值上百亿的市场!!!”。

“可是王总,我们的系统不能支持这种非实物的服务预订销售,它可能对我们原有的网站形成巨大的冲击,我们需要三个月的时间对这个业务进行全方面的评估…”

Jack Chen立即就被气势汹汹的王总打断了,“三个月的评估?我需要在两个月内就给我上线这个新业务。我们的投资人非常认可我的idea,并要求我们立即把这个项目上线,它可能会帮助我们提高明年的IPO价格。你明白吗? DO IT ASAP!”

评估

“好吧,也许这个该死的王胖子是对的。我们这个将技术与业务混在一起的乱摊子也是到了该整理整理的时候。”自言自语发了半小时牢骚后的Jack Chen终于恢复到正常状态上来了,我想我应该看看我们现在是什么样子的,为了支持这个该死的“在线为宠物医院提供在线预约的服务”的需求我们需要做出哪些改变。于是Jack Chen在白板上很快的就画出了下面的Use Case图来。

图1 原宠物商店UseCase汇总图

为了支持“在线预约”这种特殊的产品,它会影响到大部分的Use Case,具体列举如下:

  1. 商品信息需要增加“预约时间”这个属性,客户在下订单时会把它作为标识一个预约的关键要素。
  2. “在线预约”是个虚拟的商品,它可不需要真的需要去检货和包装发货,如果真的那么做啦,我就太傻了。
  3. 每个宠物医院每天都只能接受一定数量的预约,从这个概念上来说,它与实物商品有类似的库存概念。可是我该怎么去表达它们呢?
  4. 最要命的是:我真的要把这些所有受影响的Use Case都翻出来去让它们支持虚拟物品的业务吗?我怎么可能在2个月内完成这些重构?

银弹

了无生趣的Jack Chen在王总的办公室门口徘徊了N圈,还是没有勇气去迎接那一通狂风暴雨般的中英文双语版的羞辱谩骂。“也许事情是有转机的,我好象在哪里听说过有种银弹可以解决这种系统重构的问题的”。“该死,谁把Spark送给我的《领域驱动设计》垫在显示器下啦,他一直在向我布道这本书给他的项目带来的种种神奇改变,也许我也可以试试它的威力”。

“好吧,Spark,我承认你给推荐的书非常棒,你说的也很有道理。我读了它,明白并一些概念——例如:领域分割 、Entity、Service、Value Object…,可我对于该如何去做还是一头雾水。你能不能直接把你从重构项目中获得的最佳实践直接分享给我呢?不然的话,周一王胖子是不会放过交不出答案的我的!”。读完了这本书,Jack Chen觉得很有收获,但又不知道怎么开始,打个电话给领域建模的先行者Spark也许真的是解决问题最快的方法。

“什么,这个问题说来话长?不要紧,我已经在你家门口了,你同我慢慢说”,Jack Chen带着星巴克咖啡+肯德基全家桶+久久鸭脖+谄媚的笑容出现在Spark家门口。

布道

Spark听完了Jack Chen对于现状及需求的描述之后,一幅气定神闲的样子讪讪地说出“这个很简单嘛,你现在需要做的只是这样一些事情:”

  1. 用大比例结构对你的系统进行领域划分
  2. 找出这个需求影响的领域及对外接口
  3. 建立一个适合你们公司的领域驱动设计的技术框架
  4. 按照需求的紧急度来重构各个领域的设计与编码

下面我们就按照这个顺序来实践一下:

一、概要领域划分

Jack Chen立即把自己之前画的Use Case重画了一遍,然后用希冀的眼神看着Spark等待着认可。“你的错误是过于看重Case或者操作者身份,领域的划分不是基于功能或角色来进行的,通常来说我们是将内聚程度较高的Use Case归到一个上下文中。尽量使得领域自闭程度较高,并拥有相同的业务语言环境。例如基于你的Use Case图,我会画出以下的领域”

图2 宠物商店领域通道图

通道图是一个对业务领域建模非常有帮助的工具,它可以同时表达出执行序列与分片的作用。

二、找出受影响的领域与接口

从领域的角度来看,只有商品对外暴露出来的接口是会影响到各个领域,需要优先建立商品领域(ProductDomain)及读取商品信息服务接口(GetProductService)来进行重构。

之外,在【图2】 中用绿色标识出来的Use Case是由于增加支持“在线预约”这种虚拟商品所需要进行代码重构的部分。这部分工作如果工期比较紧,可以优先使用模式的方式来进行代码重构,这样也可以在之后更加容易用领域驱动设计的方法再次重构。

三、建立技术框架

这一点,是《领域驱动设计》这本书没有过多提及的内容。这个需要结合你们公司的原来技术框架用最小化改造成本最大化收益的方式来建立领域驱动的技术框架。下面是一个可以广泛使用的领域驱动的技术框架,可以在这之上增加更多的个性元素形成你公司自己的框架。

图3 领域驱动设计参考技术框架图

这个框架的各个元素基本上在 《领域驱动设计》一书中都可以找到对应的解释,但这里需要解释一下我建立这个框架的个性理解:

  1. 领域对外(页面、AJAX、ESB调用)只暴露领域服务,其它所有领域类都是包内自闭的,对外不可见。
  2. 基础仓库的引入,基础仓库是一个抽象的仓库,它封装了大量常用工具方法、业务对象生命周期维护(实体OR映射、DAO调用)、外部接口调用。可以降低业务仓库不必要的重复编码与复杂性。业务仓库是继承基础仓库的子类。
  3. 基础设施的引用,基础设施是用来承载引用非领域调用的桩,我们在使用领域驱动设计的时候往往是从一个旧的系统重构开始。这时我们不可能要求所有的业务子系统相互调用都通过Domain Service调用,这时我们可以通过Infrastructure优美的把调用封装在业务仓库的业务方法内。

四、重构受影响领域的设计与编码

图4 重构后的商品详情页类图

Spark以商品详情页这个Use Case为例展示了以领域驱动设计的重构类图:

  1. 增加行为表ProductExt用于存储商品的扩展信息,如预约时间段、预约医院。并为表建立一一对应的实体Entity。
  2. 基础仓库Repository通过Infrastructure中的DAO封装了对实体的操作,如create()、update()、delete()、findById()、findList()
  3. 商品业务仓库ProductRepository扩展了基础仓库,客户程序可以用productId为参数,通过ProductVo.getProduct()方法获得商品详细信息的业务实现,由于业务仓库的的公开方法对外返回的都是Value Object,因此不会直接暴露Entity类型给客户程序
  4. GetProductService服务类通过invoke()服务方法 对外(商品详情页面)提供服务,它通调用业务仓库中的业务方法,并将接口规格化。
  5. 事务配置在DomainService的invoke()方法上,即事务控制以Use Case为粒度进行控制。

尾声

在Spark的帮助下,Jack Chen成功的脱离了困境。现在他正在公司里积极推行自己的领域驱动设计框架,他们公司的网站正在以每三周一次的重构速度快速迭代演进。他象Spark一样,成为了一个领域驱动的布道者。


感谢郑柯对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

大型网站复杂业务持续重构之道——全程领域建模实践 by . 知秋一叶

牛的,讲解的比较清晰,支持写系列文章

跨“域”的操作应该如何安放呢? by Chen Kent

有些查询是跨域的,比如查出某用户在一月内的所有订单及每条订单的商品明细信息。从分域的角度说,这类操作应该放哪个域? 从分层的角度说,要不要搞Entity/Repository/Service 呢?

疑惑 by 曹 力文

Spark以商品详情页这个Use Case为例展示了以领域驱动设计的重构类图一下的写的东西基本没有看懂。仓库怎么返回了值对象了呢?
请问你的Entity包括行为吗?

实现部分有问题,对前半部分的分析方法感兴趣 by 张 锦华

对如何使用大比例结构对领域进行划分比较感兴趣,希望能讲解得更细致一点。领域驱动的毕竟是设计,很关心如何得出设计的这个过程。
后半部分的实现似乎有些问题,可能各有各的实现方法。但其中有二点有必要提一下:
一、领域服务(DomainService)也是用来实现业务职责的,主要是那种不属于任何一个实体(Entity)的职责,领域服务也是按业务领域进行划分的;用于协调业务领域来完成应用的叫作应用服务(AppService),它才是视图层(UI)与领域业务层(Domain)之间的桥梁,而不应该是领域服务。
二、值对象是领域模型的基本构件之一,与实体(Entity)、领域服务(DomainService)等同属于领域业务层。如果要对领域类包内自闭,即对外不可见,那也应该是应用服务(AppService)把Domain转换成数据传输对象(DTO)来返回给UI,这种对象纯粹为数据传输而存在,与业务领域没有任何关系。

Re: 跨“域”的操作应该如何安放呢? by sun bing

实体(Entity)是不分域的,就象数据库中的表你不能说它只为某一个UC服务。只有Domain Service和仓库才是有域概念的,这个UC属于哪个业务领域范畴就应该归到哪个领域的。从你说的这个UC来看,我会把它划到订单域

Re: 疑惑 by sun bing

Entity是只能在仓库中使用的,而一般供Domain Service调用于的公共仓库方法是会返回Value Object(这里也可以说DTO)以便与底层技术层解耦。

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by sun bing

1、实体(Entity)是不归属于任何领域的,它象表一样是一种信息的载体。可能被多个领域使用。属于技术实现,与业务域无关,不承担业务知识。
2、DTO属于值对象(Value Object)的一种,我不想去区别这种DTO\POJO\...种种说法的区别。我这里明确说是Value Object就是说它只是一个用于传值的对象,生命周期很短,属于使用即抛弃的对象。值对象结构与具体业务场景密切相关,所以归属于业务域。比如说展示员工信息列表所属的Value Object(不需要照片、简历...)与显示员工详细信息的Value Object(更详尽)。贴合UC的Vaule Object才能达到降低不必要数据传输,达到刚好的效果。

Re: 疑惑 by sun bing

Entity在我的实践是纯粹的POJO,没有行为,它是将业务模型与技术模型相匹配的一个桥梁而已。它不属于任何业务域

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by 张 锦华

感谢您的回复,希望有机会向您多学习。我对DDD具有深厚的兴趣,亦常实践之,但总觉不得其法,希望多指教。
1、我不是太同意实体(Entity)不属于任何领域的说法,如果是这样,我就不太明白基于DDD的方法建立的领域模型到底是什么了?代表领域业务知识的到底在哪一块?从您各个回复上下文来看,你的实体(Entity),所谓的POJO,只是个数据载体,没有行为,没有职责,将行为与职责都归于领域服务(Domain Service),这岂不是典型的贫血模型?实体没有行为与职责,岂不不具备领域建模的意义了?在我的理解中,POJO,在我们.NET里叫POCO,这种应该是不依赖于其它库或组件,只使用基础类库,具有行为职责,有血有肉的对象。
2、就我个人而言,我也不喜欢搞出太多概念,太多O来,我个人的习惯就是有需要就用,因为很多是根据项目情况而定,不是必需的。包含象DTO这种,如果没有如分布式等这些传输数据的要求,就可以不需要,UI直接使用DomainModel也是未尝不可的。而对于DDD中的值对象这种,第一它是DDD里的基本构件之一,如同实体(Entity)一样,是一个概念,不能跟DDD之外的东西等同及混淆。第二,因为一个是DDD内部构件,而象DTO之类是DomainModel之外的实现手段,如此草率等同,实有不妥。

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by 张 锦华

另外补充一下,我在前面的回复中也提到过,没有区分AppService与DomainService,文中对DomainService是也使用得不妥。这并不是实现时,哪个类,哪个文件的问题,而是概念上的清晰明确及设计实现时的职责分配问题。

疑惑too by Gang Xiao

我比较同意上面( 张 锦华)的观点:如果Entity没有行为,那么他和贫血模型到底有什么区别,不会只是把层的名字换了?
如果Entity里面长方法,从设计的松耦合以及行为与数据分离上来讲,显然也是不合理的.

Re: 跨“域”的操作应该如何安放呢? by shi andy

查询请求和命令请求需要分开设计。
命令包括:insert,update,delete,get。
查询包括:多表联合查询,统计查询。
命令请求可以用DDD设计。查询请求有时候只能单独实现,需要存储过程配合,或者需要在外面用代码聚合数据。

对领域驱动设计有兴趣有童鞋,欢迎加入群32066589 by ray lee

对领域驱动设计有兴趣有童鞋,欢迎加入群32066589

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by sun bing

只有行为才能属于领域,即UC对应的服务,所以我把它起名为DomainService,业务仓库承载着具体的服务实施,所以它也属于业务域。所以业务域对象主体只包括DomainService/Repository及为之配套的Value Object/Specfication/Factory。不要包括Entity,毕竟这个与实现关系太紧密了,而且因为大量域会使用同一个Entity所以很难说清它究竟属于哪个域

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by sun bing

对外不暴露任何的AppService,那个是具体技术实现,与领域建模的思想无关。DomainService与UC是一一对应的,这套框架是不会过早涉及技术实现方法的。

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by 张 锦华

呵呵,可能大家认识有所不同。这样回复来回复去,实在在累了,不知道是否能直接联系。我的QQ:17224096,邮箱:stwyhm@126.com。我的问题倒不在于如何实现,领域驱动的毕竟是设计。希望能向您多请教大比例结构那块,这块的相关实践与资料实在是太少了。

Re: 大型网站复杂业务持续重构之道——全程领域建模实践 by 罗 亮

刚学习领域驱动设计,支持支持,也建议写系列文章!
顶起来

Re: 疑惑 by 遥望 星空

其实我在设计系统的时候,DTO和ENTITY没有实质的区别,ENTITY就是一个领域模型(数据模型),和具体的分层没有任何关系;
我们系统中使用了MVVM+WCF+分层的架构,MVVM中使用的DTO是继承了ENTITY的类,其增加了用于UI控制的属性或方法;
总的来说,我觉得没有必要为了解耦而重复申明这些对象;

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by 遥望 星空

关于充血模型、贫血模型,我觉得不是说哪个绝对地好,哪个绝对地不好;这个必须与具体的项目、业务结合来考虑;对于分布式系统来说,贫血模型当然是最好的选择,domainmodel(DTO)是不需要传递行为或者UI数据的;但是对于非分布式系统,例如独立桌面软件或者网站来说,充血模型又是最好的选择;

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by sun bing

Domain Object如订单、客户是充血模型,当Entity只是充当PO的角色是技术模型,不能够注入业务知识。这是我的看法

Re: 疑惑 by 帮 马

Entity没有行为,这可不是Eric Evans在《DDD》中表达的意思。另Value Object也不是DTO。

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by 帮 马

赞同。

这个上第四期的周刊了,文中的观点我有点难接受,不过条条大道通罗马啊。 by 曹 力文

我比较同意张 锦华的意见。
从Eric的书中的观点及jdon中的观点。领域一般应该是充血模型且领域包含实体、值对象,且都有行为。值对象 不是数据传输对象,也不是 生命周期比较短,只是没有 标识。
DTO才是数据传输对象。

嗯,
此篇文章:需求阶段用了领域思想来组织分布式系统及模块,后又用传统的表模块(就是仓库啊)来设计及编码了,且把实体与po合为一体了。
在设计编码阶段又用了DDD的设计词汇。 并且 设计编码的时候 合理的把一些行为分类了如: 工厂、规格、仓库、服务了。

(不过一些概念与业界的有些出入,嘿嘿。)
谁说得准呢,也许在作者的系统中,这样子是最好的。开发起来最高效的。

Re: 这个上第四期的周刊了,文中的观点我有点难接受,不过条条大道通罗马啊。 by 曹 力文

传统的表模块 此 我找不到合适的 架构模式,所以就用这个了。也有点像啊。

表模块,就是有一个对象来处理此表的行为了。 是面向数据库的。

此把实体与po合为一个。一般的情况下,是有对应关系的。

Re: 跨“域”的操作应该如何安放呢? by zhao zhenguo

在领域服务层之上还应该存在一个service层,就是用来处理跨域操作的。也就是说应用层可以直接访问领域服务层,也可以访问跨域的服务层

Re: 实现部分有问题,对前半部分的分析方法感兴趣 by zhao zhenguo

非常赞同第一点;对于第二点有些异议,VO(值对象)模型不一定隶属于领域层,不带有任何业务逻辑的Entity是VO,DTO也是VO,如你所说DTO其实是不属于领域层的。

Re: 疑惑too by zhao zhenguo

对于Entity或者pojo的贫血、涨血、失血、充血等问题业界也一直有争论(具体的场景具体分析),另外并不是说行为必须和数据分离,如果所有的都是行为和数据分离,那就变成纯粹的面向过程了

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

27 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT