人物介绍:
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,具体列举如下:
- 商品信息需要增加“预约时间”这个属性,客户在下订单时会把它作为标识一个预约的关键要素。
- “在线预约”是个虚拟的商品,它可不需要真的需要去检货和包装发货,如果真的那么做啦,我就太傻了。
- 每个宠物医院每天都只能接受一定数量的预约,从这个概念上来说,它与实物商品有类似的库存概念。可是我该怎么去表达它们呢?
- 最要命的是:我真的要把这些所有受影响的 Use Case 都翻出来去让它们支持虚拟物品的业务吗?我怎么可能在 2 个月内完成这些重构?
银弹
了无生趣的 Jack Chen 在王总的办公室门口徘徊了 N 圈,还是没有勇气去迎接那一通狂风暴雨般的中英文双语版的羞辱谩骂。“也许事情是有转机的,我好象在哪里听说过有种银弹可以解决这种系统重构的问题的”。“该死,谁把 Spark 送给我的《领域驱动设计》垫在显示器下啦,他一直在向我布道这本书给他的项目带来的种种神奇改变,也许我也可以试试它的威力”。
“好吧,Spark,我承认你给推荐的书非常棒,你说的也很有道理。我读了它,明白并一些概念——例如:领域分割 、Entity、Service、Value Object…,可我对于该如何去做还是一头雾水。你能不能直接把你从重构项目中获得的最佳实践直接分享给我呢?不然的话,周一王胖子是不会放过交不出答案的我的!”。读完了这本书,Jack Chen 觉得很有收获,但又不知道怎么开始,打个电话给领域建模的先行者Spark 也许真的是解决问题最快的方法。
“什么,这个问题说来话长?不要紧,我已经在你家门口了,你同我慢慢说”,Jack Chen 带着星巴克咖啡+ 肯德基全家桶+ 久久鸭脖+ 谄媚的笑容出现在Spark 家门口。
布道
Spark 听完了 Jack Chen 对于现状及需求的描述之后,一幅气定神闲的样子讪讪地说出“这个很简单嘛,你现在需要做的只是这样一些事情:”
- 用大比例结构对你的系统进行领域划分
- 找出这个需求影响的领域及对外接口
- 建立一个适合你们公司的领域驱动设计的技术框架
- 按照需求的紧急度来重构各个领域的设计与编码
下面我们就按照这个顺序来实践一下:
一、概要领域划分
Jack Chen 立即把自己之前画的 Use Case 重画了一遍,然后用希冀的眼神看着 Spark 等待着认可。“你的错误是过于看重 Case 或者操作者身份,领域的划分不是基于功能或角色来进行的,通常来说我们是将内聚程度较高的 Use Case 归到一个上下文中。尽量使得领域自闭程度较高,并拥有相同的业务语言环境。例如基于你的 Use Case 图,我会画出以下的领域”
图 2 宠物商店领域通道图
通道图是一个对业务领域建模非常有帮助的工具,它可以同时表达出执行序列与分片的作用。
二、找出受影响的领域与接口
从领域的角度来看,只有商品对外暴露出来的接口是会影响到各个领域,需要优先建立商品领域(ProductDomain)及读取商品信息服务接口(GetProductService)来进行重构。
之外,在【图 2】 中用绿色标识出来的 Use Case 是由于增加支持“在线预约”这种虚拟商品所需要进行代码重构的部分。这部分工作如果工期比较紧,可以优先使用模式的方式来进行代码重构,这样也可以在之后更加容易用领域驱动设计的方法再次重构。
三、建立技术框架
这一点,是《领域驱动设计》这本书没有过多提及的内容。这个需要结合你们公司的原来技术框架用最小化改造成本最大化收益的方式来建立领域驱动的技术框架。下面是一个可以广泛使用的领域驱动的技术框架,可以在这之上增加更多的个性元素形成你公司自己的框架。
图 3 领域驱动设计参考技术框架图
这个框架的各个元素基本上在 《领域驱动设计》一书中都可以找到对应的解释,但这里需要解释一下我建立这个框架的个性理解:
- 领域对外(页面、AJAX、ESB 调用)只暴露领域服务,其它所有领域类都是包内自闭的,对外不可见。
- 基础仓库的引入,基础仓库是一个抽象的仓库,它封装了大量常用工具方法、业务对象生命周期维护(实体 OR 映射、DAO 调用)、外部接口调用。可以降低业务仓库不必要的重复编码与复杂性。业务仓库是继承基础仓库的子类。
- 基础设施的引用,基础设施是用来承载引用非领域调用的桩,我们在使用领域驱动设计的时候往往是从一个旧的系统重构开始。这时我们不可能要求所有的业务子系统相互调用都通过 Domain Service 调用,这时我们可以通过 Infrastructure 优美的把调用封装在业务仓库的业务方法内。
四、重构受影响领域的设计与编码
图4 重构后的商品详情页类图
Spark 以商品详情页这个 Use Case 为例展示了以领域驱动设计的重构类图:
- 增加行为表 ProductExt 用于存储商品的扩展信息,如预约时间段、预约医院。并为表建立一一对应的实体 Entity。
- 基础仓库 Repository 通过 Infrastructure 中的 DAO 封装了对实体的操作,如 create()、update()、delete()、findById()、findList()
- 商品业务仓库 ProductRepository 扩展了基础仓库,客户程序可以用 productId 为参数,通过 ProductVo.getProduct() 方法获得商品详细信息的业务实现,由于业务仓库的的公开方法对外返回的都是 Value Object,因此不会直接暴露 Entity 类型给客户程序。
- GetProductService 服务类通过 invoke() 服务方法 对外(商品详情页面)提供服务,它通调用业务仓库中的业务方法,并将接口规格化。
- 事务配置在 DomainService 的 invoke() 方法上,即事务控制以 Use Case 为粒度进行控制。
尾声
在 Spark 的帮助下,Jack Chen 成功的脱离了困境。现在他正在公司里积极推行自己的领域驱动设计框架,他们公司的网站正在以每三周一次的重构速度快速迭代演进。他象 Spark 一样,成为了一个领域驱动的布道者。
感谢郑柯对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论