上一篇:《IDDD 实现领域驱动设计-由贫血导致的失忆症

这篇博文是对《实现领域驱动设计》第一章后半部分内容的理解。


Domain Experts-领域专家

这节点内容是昨天的一个讨论引发的思考。

什么是领域专家?简单来说,就是对某一业务领域精通的人,这个人可以是医生、学者、作家、艺术家等等,不管是什么职业,什么身份,只要对某一业务领域精通,都可以称之为领域专家。这样说可能会让你感到茫然,我举一个例子,比如你们软件公司要开发一套快递行业的业务系统,然后你需要到实际企业去了解业务流程等等,暂时把这个实际企业想象成很小(非三通一达),那么你到这个企业第一时间找的是谁呢?准确来说,应该是这个公司的 CEO,因为只有他最最了解他们公司的业务,毕竟是他创办的公司,CEO 不了解,还有谁还了解呢,那么,这个公司的 CEO 就可以看作是领域专家。CEO 一般是蛮忙的,有很多的琐事需要处理,所以,在你和他聊天了解业务的时候,最好是先准备一杯咖啡!

当我们开发人员自己开发一套系统的时候,在开发团队之间,领域专家的概念就慢慢淡化了,为什么?因为领域专家变成了我们开发人员自己,自己给自己布置业务,然后自己再去完成,这样虽然很高效,因为没有非技术人员的参与沟通,但是这样就会造成一些问题,比如,开发人员在思考业务流程的时候,会按照开发人员的思路去理解,比如,一个简单的业务操作描述,开发人员会首先想到的什么呢?一个表单和一个 Button,然后就是对这个表单和 Button 操作的具体实现了,等项目开发完成后,需要交付真正的客户去检验,客户让你演示这个业务操作,然后你就开始对表单和 Button 进行操作了,说这就是业务操作,但是,客户突然来一句:我们不要表单和 Button 操作,UI 需要重新搞,这时候,你就傻眼了,因为你所有的内容代码实现都是围绕着表单和 Button。说了这么多,到底是什么意思呢?在这个过程中,你并不了解这个业务操作背后所蕴含的业务含义,首先,业务不是 UI,UI 只不过是业务的一部分体现,有时候,业务仅仅只是领域专家的一段描述,开发人员需要对这个业务描述,进行一点一点的抽离,把术语和操作分离开,然后再和领域专家进行深入的探讨,这个过程可能会花很多的时间,但是是非常重要的,做完这些前期工作,你再去实现业务操作,你会发现,不管 UI 如何变化,这个业务操作的本质是没有发生变化的,也就是说你的内部代码不需要进行修改,UI 修改那就交给前端工程师就可以了,和你没太大关系。总的来说,就是不要让 UI 驱动你开发,而是让业务驱动你开发。

对上面的内容,我还需要补充一点,就是开发人员需要领域专家,开发人员和领域专家的身份最好不要重叠,要不然会造成一系列的问题,还有就是,在整个领域驱动设计的过程中,开发人员和领域专家的地位是相同的,不要有任何的轻视心态,要用平等的心态去沟通交流。领域专家的概念,让我想到一个很相似的事,就是苹果在开发一个产品的时候,会请很多的非技术人员参与,这些人遍布各行各业,医生、学者、作家、艺术家等等,苹果为什么要请他们,就是想让他们参与产品的设计,因为他们就是产品的使用者,他们提出的想法就是实实在在的用户建议,这个产品开发过程,其实就可以看作是领域(产品)驱动设计,这些参与产品设计的非技术人员,就可以看作是领域(产品)专家。

一个简单业务用例的回顾和理解

这个简单业务用例描述是这样的:一个 Scrum 模型,我们需要将一个待定项(Backlog Item)提交到冲刺(Sprint)中去。

这是最简答的描述,没有经过和领域专家进行深入沟通的,Scrum 是敏捷开发中的概念,这个就不说明了,因为我也不懂,你只需要知道上面的操作就可以了,一般的实现方式(属性访问):

public class BacklogItem extends Entity {
private SprintId sprintId;
private BacklogItemStatusType status;
...
public void setSprintId(SprintId sprintId) {
this.sprintId = sprintId;
} public void setStatus(BacklogItemStatusType status) {
this.status = status;
}
...
}

客户端调用:

// client commits the backlog item to a sprint
// by setting its sprintId and status backlogItem.setSprintId(sprintId);
backlogItem.setStatus(BacklogItemStatusType.COMMITTED);

上面的实现过程,完全和上一篇 saveCustomer 的实现方式一样,这样做没什么不可以,因为我也这样干过,只是你会总感觉有哪些不对劲的地方,首先,在实现待定项提交到冲刺这个操作的时候,你首先查看的是 BacklogItem 中的属性,然后就是对这个属性进行设置,在这个过程中,你忘记了你实现的是一个行为操作,而不是一个属性赋值操作,这样说来,是不是有点脚本模式开发,还有就是如果客户端第二个属性赋值 setStatus 出现了错误,因为第一个 setSprintId 已经成功完成,这个该怎么进行处理,即使有处理,这个操作也完全放在了客户端去完成,像 saveCustomer 一样,如果再增加一个属性赋值操作,你的实现将越改越乱,最重要的是,再客户端暴露了 BacklogItem 模型的具体结构,这个应该是要避免的。

我们再来看另一种实现方式:

public class BacklogItem extends Entity {
private SprintId sprintId;
private BacklogItemStatusType status;
... public void commitTo(Sprint aSprint) {
if (!this.isScheduledForRelease()) {
throw new IllegalStateException(
"Must be scheduled for release to commit to sprint.");
} if (this.isCommittedToSprint()) {
if (!aSprint.sprintId().equals(this.sprintId())) {
this.uncommitFromSprint();
}
} this.elevateStatusWith(BacklogItemStatusType.COMMITTED); this.setSprintId(aSprint.sprintId()); DomainEventPublisher
.instance()
.publish(new BacklogItemCommitted(
this.tenant(),
this.backlogItemId(),
this.sprintId()));
}
...
}

客户端调用:

// client commits the backlog item to a sprint
// by using a domain-specific behavior backlogItem.commitTo(sprint);

将第一种是实现方式出现的问题,再和第二种方式进行比较,你会发现,第二种实现方式完全避免掉了,在开始的时候,我们说了,这是一个最简单的业务操作描述,没有和领域专家进行深入探讨和交流,如果进行探讨和交流的话,最后详细、准确的业务操作描述,应该是这样:

  • 允许将每一个待定项提交到冲刺中,只有在一个待定项位于发布计划(Release)中时才能进行提交,如果一个待定项已经提交到了另外一个冲刺中,那么需要先将其回收,提交完成时,通知相关客户方。

对于一个详细、准确的业务操作描述,如何进行确定下来,作者进行了如下总结:

  1. 对于你目前正在工作的业务领域,思考一下模型中的通用术语和业务操作。
  2. 将术语写在白板上。
  3. 然后,将项目中所用到的短语也写下来。
  4. 与真正的领域专家交流一下,看看哪些词汇是可以改善的(记得带上咖啡哦)。

我们再来分析一下上面第二种实现方式,希望可以抽离出一些对自己有所帮助的理解,首先,读上面的业务操作描述,然后再和实现代码进行对比,你会发现,它们之间的关系是完全契合的,在上一篇中,我们说过,设计就是代码,代码就是设计,这种设计就是一种通用语言,开发人员和领域专家都能懂的通用语言。

在第二种实现的方式中,有两个关键词:commitTo 和 DomainEventPublisher,DomainEventPublisher 是领域事件(Domain Event),这个不要和领域服务(Domain Service)混淆,领域事件我没有使用过,后面再进行学习,你暂时可以把它看作是操作完成后的消息推送者。commitTo 是 BacklogItem 模型中的一个行为,意为提交,你可能会这样想:待定项怎么会有行为呢?它又不是人,我觉得这个很有意思,记得在之前做消息模型设计的时候,一直不确定的一点是发消息这个操作该如何设计?是消息实体的一个行为操作,还是发件人的一个行为操作,又或者是独立出来的一个领域服务(最后结果),在这个设计确定的过程中,我们会进行多次讨论,但有一点需要进行明确的是,不只是具有“生命”的实体,才具有行为操作,就像消息模型中的操作人,你自然会联想到现实生活中的发件人、收件人等等,认为只有人才会有一些行为操作,但是实际上,在软件系统中,一切的模型都有可能是行为操作,你要摒弃现实生活对你的影响,就像上面待定项的提交操作,如果是我设计的话,我会创建一个领域服务进行行为操作,因为,在我的认知中,待定项不具有行为操作,但显然并不是这样,为什么要这样设计?现在还说不出个所以然,以后再慢慢体会。

DDD 并不笨重(测试驱动)

DDD(领域驱动设计)和 TDD(测试驱动开发),这两者有什么关系?我记得在之前的博文中有提到这一点,我的观点是,DDD 和 TDD 可以之间可以产生一些微妙的化学反应,并不一定要强制的去区分它们之间的关系,比如,如果你的 DDD 项目中,使用了 TDD,并不能说明你的项目就不是 DDD 模式了,其实,TDD 可以对 DDD 进行一些补充,或者可以让你的项目,在使用 DDD 的时候,变得如鱼得水。关于它们两者的关系,作者简单说明了一下观点:DDD 也倾向于“测试先行,逐步改进”的设计思路,他们可能有细微的区别,但是基本思路是一样的,DDD 采用的是一种“敏捷的”方式进行软件开发的。

可以采取的步骤:

  1. 编写测试代码以模拟客户代码是如何使用该领域对象的。
  2. 创建该领域对象以使测试代码能够编译通过。
  3. 同时对测试和领域对象进行重构,直到测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。
  4. 实现领域对象的行为,直到测试通过为止,再对实现代码进行重构。
  5. 向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。

具体再说明一下,像上面的待定项提交业务操作,可以完全先写一个测试代码,如下:

[test]
public void backlogItemCommit() {
...
}

这个测试代码,其实就是领域专家想要的,他不管你是如何具体实现的,他关心的是有没有这个业务操作,以及这个业务操作完成的结果,也就是说,测试代码可以很好的反应领域专家所描述的业务操作,那有人可能就会说了:你这就不是 DDD 了,而是 TDD,表明看上去,好像确实如此,但是不能说写个测试代码就是 TDD 开发,而去测试代码并不能反映领域模型,他只是一种辅助方式,你可以把它看作是通用语言的一种,可以帮助你和领域专家进行沟通,也可以加快你的开发速度,又或者可以帮助你完善你的领域模型设计。对应某一业务操作的测试代码,也不是一成不变的,它需要开发人员和领域专家的持续沟通和改进,测试代码就是他们进行通用语言的一种表现形势,使用测试代码的好处就是,它可以很好的表现业务需求,当然你也可以使用 UI,这些都不过是通用语言的一种罢了。

在读《DDD 并不笨重》这一小节点内容的时候,我是很有感触和共鸣的,因为我在之前短消息开发的时候,就曾这样搞过,比如,新建一个与 Domain 对应的 Domain.Tests 项目,这个 Domain.Tests 就是你和领域专家进行沟通的一个桥梁。

对于这个节点内容,可能每个人都有自己的理解,如果大家有不同的想法,欢迎探讨交流,就记录到这!

IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解的更多相关文章

  1. IDDD 实现领域驱动设计-理解领域和子域

    上一篇:<IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解> 在<实现领域驱动设计>第二章的前半部分内容中,提到领域和子域的概念,并且作者把这两者又进行了细致的区分,其 ...

  2. IDDD 实现领域驱动设计-架构之经典分层

    上一篇:<IDDD 实现领域驱动设计-上下文映射图及其相关概念> 在<实现领域驱动设计>书中,分层的概念作者讲述的很少,也就几页的内容,但对于我来说,有很多的感触需要诉说.之前 ...

  3. IDDD 实现领域驱动设计-一个简单的 CQRS 示例

    上一篇:<IDDD 实现领域驱动设计-CQRS(命令查询职责分离)和 EDA(事件驱动架构)> 学习架构知识,需要有一些功底和经验,要不然你会和我一样吃力,CQRS.EDA.ES.Saga ...

  4. IDDD 实现领域驱动设计-理解限界上下文

    上一篇:<IDDD 实现领域驱动设计-理解领域和子域> <实现领域驱动设计>前两章内容,基本上读完了,和<领域驱动设计>不同的是,它把很多的概念都放在前面进行讲述了 ...

  5. IDDD 实现领域驱动设计-CQRS(命令查询职责分离)和 EDA(事件驱动架构)

    上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动架构 Domin Event-领域事件 Long-Runni ...

  6. IDDD 实现领域驱动设计-上下文映射图及其相关概念

    上一篇:<IDDD 实现领域驱动设计-理解限界上下文> 距离上一篇有几天时间了,<实现领域驱动设计>第三章的内容都是围绕一个词-上下文映射图,我大概断断续续看了几天,总共看了两 ...

  7. IDDD 实现领域驱动设计-SOA、REST 和六边形架构

    上一篇:<IDDD 实现领域驱动设计-架构之经典分层> 阅读目录: SOA-面向服务架构 REST 与 RESTful 资源(Resources) 状态(State) 六边形架构 DDD ...

  8. IDDD 实现领域驱动设计-由贫血导致的失忆症

    啰嗦几句 年前的时候,在和 netfocus 兄,以及对 DDD 感兴趣园友的探讨过程中,我发现自己有很多不足的地方,对 DDD 的了解也只是皮毛而已,代码写的少,DDD 的基本概念也不是很清楚,空有 ...

  9. 【DDD】使用领域驱动设计思想实现业务系统

    最近新接了一个业务系统——社区服务系统,为了快速熟悉和梳理老系统的业务逻辑和代码,同时对老系统代码做一些优化,于是打算花上一个月时间不间断地对老系统服务进行重构.同时,考虑到社区业务的复杂性,想起了之 ...

随机推荐

  1. c#List移除列表中的元素

    对于一个List<T>对象来说移除其中的元素是常用的功能.自己总结了一下,列出自己所知的几种方法. class Program { static void Main(string[] ar ...

  2. Android :fragment介绍

    一.关于Fragmemt 1.Fragment(片段),主要是为了支持更多的动态和灵活的用户界面设计,如平板电脑.Fragment允许组合和交换用户界面组件,而不需要更改视图层次结构.通过把Activ ...

  3. iOS Block理解

    以前看到Block觉得也没什么,不就是类似函数的东西,这东西在C#里就是委托,在Java里就是块,有什么稀奇的.但看到一点进阶的内容后,发现这个东西确实有用. 所以做下总结. 一.块的基本用法 块的语 ...

  4. Javascript初学篇章_6(BOM)

    BOM 浏览器对象模型 BOM (浏览器对象模型),它提供了与浏览器窗口进行交互的对象 一.window对象 Window对 象表示整个浏览器窗口. 1.系统消息框 alert() alert('he ...

  5. 平凡的KTV后台,不平凡的KTV数据

    之前就是说过“一个项目有很多重要的步骤以及功能”,那我们现在就来看看对于KTV项目来说:后台是处于什么样的重要作用! 首先就得了解KTV后台的一些功能了: 1.歌曲管理 .歌手管理 .设置资源路径 2 ...

  6. java面向对象_构造器

    构造器(构造方法):是类中定义的方法. 1)常常用于给成员变量赋值: 2)与类同名,没有返回值类型,也不能写void: 3)在创建对象时被自动调用.所以构造方法的访问修饰符要用public,才能被自动 ...

  7. USACO翻译:USACO 2013 DEC Silver三题

    USACO 2013 DEC SILVER 一.题目概览 中文题目名称 挤奶调度 农场航线 贝西洗牌 英文题目名称 msched vacation shuffle 可执行文件名 msched vaca ...

  8. 编译安装PHP的参数 --with-mysql --with-mysqli --with-apxs2默认路径

    编译安装PHP时指定如下几个参数说明: --with-apxs2=/usr/local/apache/bin/apxs //整合apache,apxs功能是使用mod_so中的LoadModule指令 ...

  9. DataTable扩展方法ToList<T>()、ToJSON()、ToArrayList()

    /// <summary> /// 扩展方法类 /// </summary> public static class CommonExtension { /// <sum ...

  10. 领域驱动设计(DDD)部分核心概念的个人理解

    领域驱动设计(DDD)是一种基于模型驱动的软件设计方式.它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题.Eric Ivans为领域驱动设计提出了大量的最佳实践 ...