《平凡的DDD》第二弹: DDD中的建模

1,485 阅读11分钟

这事从哪说起呢?还是从儿童牙科医生李大夫那说起吧。

一、对话领域专家

李大夫是一位在东湖颇为知名的儿童牙科医生,他拥有一个自己的牙科诊所,以及相关的设备和团队。近些年来,随着人们生活水平的不断提高,家长们对孩子的牙齿健康问题越来越关注。所以,李大夫的诊所生意越来越好,每到节假日人满为患,应接不暇。于是乎,为了让诊所健康地运转,李大夫想到了用科技手段来帮助自己管理诊所:他要开发一套小程序,毕竟不差钱!

马龙湾的马先生打小就子承父业,是一位35出头的行业老专家,恰好那天带孩子前去看牙,听到李大夫有此心意后,他颇为激动。想到李大夫治好了孩子的蛀牙问题,自己又是行家,更能捞一笔,便主动提出希望和李大夫合作,希望能以小程序为抓手,帮助李大夫实现商业闭环,为诊所科技赋能。一阵寒暄后,李大夫把马先生请到了隔壁小房间,谈了他的想法。

李大夫:我想搞个预约,家长带孩子来看病前,可以微信扫码预约。 马先生:对,这个得有,然后呢? 李大夫:预约时得说清楚是预约的目的,比如是检查还是复诊,或者还是手术等等。 马先生:可以。不同的预约类型,有什么限制吗? 李大夫:如果是手术的话,预约的时候要看预约的日期有没有档期,还得看人手够不够。 李大夫:在我们这里就诊的病人,可以通过小程序查看病历和检查结果等。哦,对了,预约要到期时,要提前一天给家长发送短信和微信通知。 马先生:没问题。最好当天再发送一条短信信息再次提醒下。 李大夫:对。另外,最好能支持线上支付。患者在治疗期间的医疗费用情况可以在自己的账户里查看,需要发票的话,也可以直接开发票。 ...

马先生听完李大夫的描述,一边想着开价多少合适,一边脑袋里冒出了一些关键的词:

预约家长孩子检查复诊手术医生病历支付发票账户...

作为一名老专家,马先生在这个对话过程表现了较高的职业素养:他没有着急写代码,也没有像年轻人那样使用程序语言和李大夫进行沟通,他用的是诊所大夫能听得懂的语言。因为马先生十分清楚,在目前这个阶段,重点是弄清楚牙科诊所这个领域是如何运作的,而不是软件如何运作

这正所谓对于一个开发者而言,失败往往只有两点:把事情做错了,或者做错了事情(We build the thing wrong, or We build the wrong thing)。所以马先生把理解领域运作放在了首位。

二、从对话中了解领域

回到家后,马先生根据目前所掌握的信息,着手开始设计牙科诊所小程序的领域模型。于是,他画了下面的这张图。

画完领域图稿后,马先生又在图中标记处了相应的核心域支撑子域通用子域

如果你之前不了解这几种域的区别,这里做个简单的补充说明。

在一个DDD项目中,会有很多的限界上下。这些限界上下文中,有一个或多个会成为核心域,而其他的限界上下文中则会出现许多不同的子域。在和领域专家沟通后,首要的是识别出领域,其次是根据领域识别出其中的核心域、支撑子域和通用子域,只有这样才能在后续的开发和资源安排中做到有的放矢

  • 核心域(Core Domain):所谓核心域,它是一个唯一的、定义明确的领域模型,不需要太过复杂的理解,你只需要知道对待核心域,要对它进行战略投资,并在一个明确的限界上下文中投入大量资源去精心打磨通用语言。它是组织中最重要的项目,某种程度上说,它是你与竞争者的区别所在。换句话说,任何组织都无法在所有领域出类拔萃,所以必须把核心领域打造成组织的核心竞争力。
  • 支撑子域(Supporting Subdomain):对于支撑子域的建模场景,一般提倡的是定制开发。也就是说,它也挺重要,但可能没有现成的方案,又不是核心领域,所以可以定制开发,或者说外包开发。但要注意的是,虽说是支撑子域,在战略投资上不如核心域,但它是核心域成功的基础,也需要认真对待。
  • 通用子域(Generic Subdomain):所谓通用子域,你可以简单理解为可以购买现成的,也可以外包实现。总之,它没那么重要,又不可或缺,不必投入太多资源。

一般来说,有效地识别出不同的子域,可以更加合理地分配组织的资源,把优秀资源投入到重要的事情中,这也是DDD的重要价值体现。再说一遍,DDD的价值,从来都不是只在代码里,代码只是DDD的最后一道坎。

三、关注子域

理解领域并识别出不同的子域后,马先生要做的事情可就多了。不过,一口也吃不成一个胖子,还是得逐个子域击破。比如针对预约子域,马先生进一步梳理了他与李大夫对话中的要点:

  • 家长在带孩子来看病前,需要先预约;
  • 预约可以是检查、复诊、手术等;
  • 不同类型的预约,需要不同的资源,并且需要检查资源的排版情况;
  • 家长与患者不是一个概念;
  • 一个家长可以有多个孩子。

随后,马先生画了下面这张领域模型图:

在关注具体子域的过程中,除了精心的模型设计,很重要的一点是要继续和领域专家进行沟通,深挖其中的细节。此外,在聚焦子域时,往往还会牵扯出其他的领域以及相关的领域逻辑上的联系,比如这里的手术室,而手术室的排班表则会直接影响到预约。

四、创建限界上下文

在设计领域模型时,有一个非常重要的点,那就是我的模型在哪里才是有效的?它必然不会是放之四海而皆准。于是,为了给模型一个合理的解释,“限界上下文”便闪亮登场了。

**限界上下文(Bounded Context)**本身并不难理解。举个例子,我们都看过不同的地图,不同的地图都会有“图例”,用以解释地图中一些标记或颜色的含义,比如虚线表示什么、实线表示什么。很明显,这些图例仅能在本地图中使用,换个地图,它的含义可能就变了。

同样的,以马先生小程序中的Client为例,在“预约”中和在“账户”中,同是Client,但它们的含义并不完全相同,模型设计也并不一样。而不同上下文中的模型,只能在当前的边界内解释。比如“账户”的Client模型中有“信用卡Credit Card”属性,同样很显然,这个字段放到“预约”的Client则无法解释,说不通。

以上,就是限界上下文的通俗解释,看起来比较抽象,其实很好理解,就是看住你的模型,不要让它乱跑。

所以,在设计领域模型时,请记住,给它一个边界。

五、子域和限界上下文有什么不同

那么,上面谈到了子域,也谈到了限界上下文。那这两个有什么关系和不同呢?说起来,它们是完全不同的东西,但彼此之间有存在联系,容易引起困惑。

对此,Eric曾经亲口说过一句话:

Sub-Domain is a problem domain concept, and Bounded Context is a solution domain concept.

Eric的意思是,子域是一种问题空间的概念,而限界上下文是解空间的概念。听起来是不是也有点形而上的感觉?你可以简单理解为,子域所描述的是业务领域中信息和领域活动,而限界上下文表达的则是子域在软件中的体现,它是子域代码化的一种方式。比如领域模型中appointment表示的是子域,而代码目录中的appointment文件夹表示的则是appointment上下文,本质上指的还是子域。

六、理解上下文映射

前面说过,在一个DDD项目中会有很多的子域,对应的也就会有很多的限界上下文,而这些限界上下文并不是彼此孤立的存在,它们之间是存在某种集成关系的,这种关系就叫上下文映射(Context Mapping)

比如,在马先生的小程序里,用户在预约完成后或者预约到期时,需要给用户发送通知,这便是预约子域依赖于通知子域。

上下文映射所体现的是两个子域之间的协作关系,而如何定义两者之间恰当的关系,则是在模型设计中的一个重要考量。举个反例,如果子域A与子域B原本是合作关系,分别由两个团队负责完成,彼此一荣俱荣,一损俱损。假如你是子域A的负责人,你非要把子域B设计成你的跟随者,都听你的,那么可能的结果就是无论是在技术架构上还是组织层面上,你把两个团队都拖入泥潭。

再比如,子域C的上下文原本可以为子域D和子域E的上下文所共用,结果D和E各行其道,各自实现了一套C的上下文,造成不必要的代码重复和资源浪费。

以上两个例子中的映射关系都是错误的。所以,你可以不考虑“上下文映射”这个抽象的概念,但你要考虑到两个模块的协作关系。

关于上下文的映射类型有好几种,这里不作赘述和列举,主要是个人认为没必要,有兴趣的可以自行检索资料。

七、限界上下文中的通用语言

在上一篇文章中,已经强调过**通用语言(Ubiquitous Language)**是DDD的重要组成部分,事实上也可能是最基本、最需要的实践,也最容易实践的部分。

通用语言关系到的是在一个项目中,利益相关方能否有效沟通。作为开发者,是项目中的重要角色,直接影响到项目成败。我们在与各方沟通时所使用的语言,和我们在建模时所使用的语言,是我们与包括领域专家在内的各方达成共识的关键所在。

拥有一套通用语言,可以有效避免组织内部和软件内部的一些不必要的混乱和翻译。比较常见的混乱是,产品与技术是两套话语体系,前端与服务端是两套话语体系,而服务端与服务端之间可能还有多套话语体系,毕竟大家所关心的都是所谓的结果,而不是过程。这些混乱的语言和翻译的缺失,基本难以成就优秀的架构,更不要说彼此认可。

总之,通用语言是DDD的基本实践之一,但价值有明显,它应该无处不在:文档里、代码里、日常沟通里、电话会议里、白板上等等。另外要注意的是,如果通用语言发生了变更,你的模型也要随之同步变更

八、应用中的限界上下文

关于DDD的代码组织,并没有统一的范式,原则上要能体现出领域模型、领域逻辑以及限界上下文,在此基础上可以结合其他一些合适的架构模式。上图想体现的是应用代码中的限界上下文,关于DDD在代码层面的更多实践,后续文章继续聊。

小结

在本文中,我们通过与领域专家的对话来理解领域,并对领域进行子域划分,进而了解了限界上下文和通用语言。文本包含了一些DDD中的概念和术语,但本文无意机械翻译和传递,有兴趣的可以自行整理消化。


关注公众号【MetaThoughts】,及时获取专栏文章更新通知。

如果本文对你有帮助,欢迎点赞关注