《实现领域驱动设计》笔记——领域、子域和限界上下文

  总览

  从广义上讲,领域(Domain)即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场,然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及在其中所进行的活动便是领域。当你为某个组织开发软件时,你面对的便是这个组织的领域。这个领域对于你来说应该是明晰的,因为你在这个领域中工作。

  有一点需要注意的是,“领域”这个词可能承载了太多含义。领域既可以表示整个业务系统,也可以表示其中的某个核心域或者支撑子域。

  由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建一个单一的、内聚的、全功能的式的模型。然而,这并不是我们使用DDD的目标。正好相反,在DDD中,一个领域被分为若干子域,领域模型在限界上下文中完成开发。事实上,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。试图创建一个全功能的领域模型是非常困难的,并且非常容易失败。

  那么,既然领域模型不能包含整个业务系统,我们应该如何划分领域模型?

  几乎所有软件的领域都包含多个子域,这和软件系统本身的复杂性没有太大关系。有时,一个业务系统的成功取决于它所提供的多种功能,而将这些功能分开对待是有好处的。

 

  工作中的子域和限界上下文

  很多软件开发者都认为将所有东西都放在一个系统里面是一件好事。他们会想:“我对电商系统了如指掌,我相信这个系统可以满足任何人的需求。”这是具有欺骗性的,因为你不管向系统中添加多少功能,你都无法满足每一个潜在客户的需求。此外如果不通过子域对软件系统进行划分,事情将变得更加繁琐,因为系统中的各个部分都是紧密联系在一起的。

  子域并不是一定要做的很大,并且包含很多功能。有时,子域可以简单到只包含一套算法,这套算法可能对业务系统非常重要,但是并不包含在核心域之中。在正确实施DDD的情况下,这种简单的子域可以以模块(Module)的形式从核心域中分离出来,而不需要包含在笨重的子系统组件中。

  在实施DDD的时候,我们致力于将限界上下文中领域模型所用到的每一个术语都进行限界划分。这种限界主要是语言层面上的上下文边界,也是实现DDD的关键。

  需要注意的是,一个限界上下文并一定是只包含在一个子域中,但这是可能的。

  一个子域对应一个限界上下文。

 

  将关注点放在核心域上

  对于核心域,它是整个业务领域的一部分,也是业务成功的主要促成因素。从战略层面上讲,企业应该在核心域上胜人一筹。在实施DDD的过程中,你将主要关注于核心域。

  有时,我们会创建或者购买某个限界上下文来支撑我们的业务。如果这样的限界上下文对应着业务的某些重要方面,但却不是核心,那么它便是一个支撑子域。创建支撑子域的原因在于它们专注于业务的某个方面,否则,如果一个子域被用于整个业务系统,那么这个子域便是通用子域。我们并不是说支撑子域和通用子域是不重要的,它们是重要的,只是我们对它们的要求没有核心域那么高。

 

  战略设计为什么重要

  如果不了解战略设计的基础,会将关注点放在实体值对象上,从而缺少一种更广阔的视野。这种做法还很有可能导致大泥球架构。

  团队真正需要理解的是他们正在开发的领域、子领域和限界上下文,他们需要战略建模的思维模式。

 

  现实世界中领域和子域

  领域中还同时存在问题空间(problem space)和解决方案空间(solution space)。在问题空间中,我们思考的是业务所面临的挑战,而在解决方案空间中,我们思考如何实现软件以解决这些业务挑战。以下是如何将这两者应用到我们已经学过的知识中:

  • 问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注域当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速地浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
  • 解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。

  通常,我们希望将子域一对一地对应到限界上下文。这种做法显式地将领域模型分离到不同的业务板块中,并将问题空间和解决方案空间融合在一起。在实践中,这种做法并不总是可能的,但通过新的努力,我们是可以做到这一点。

  让我们考虑一个遗留系统,它有可能是个大泥球,其中子域和限界上下文存在相交的地方。在一个大型的企业中,通过对问题空间的评估,我们可以减少错误,进而降低成本。我们可以在概念上使用两个或者多个子域来分解限界上下文,或者将多个限界上下文包含在同一个子域中。

 

  在我们实施某个解决方案之前,我们需要对问题空间和解决方案空间进行评估。为了保证你的项目朝着正确的方向行进,你需要先回答以下问题:

  • 这个战略核心域的名字是什么,它的目标是什么?
  • 这个战略核心域中包含哪些概念?
  • 如何安排项目人员?
  • 你能组建一支合适的团队吗?

  如果你不了解核心域的目标及其所需的支撑子域,那么你是不能从核心域中得到好处的,同时你也无法避免由此带来的陷阱。因此,我们需要全面地对问题空间进行评估,并确保所有的利益相关方在核心域的目标上都达成一致。

 

  在理解了问题空间之后,我们来看看解决方案空间。对于问题空间的评估也是有益于理解解决方案空间的。解决方案空间在很大程度上受到现有系统和技术的影响。这里,我们应该根据分离的限界上下文仔细地思考,考虑以下问题:

  • 有哪些软件资产已经存在,它们可以重用吗?
  • 哪些资产是需要创建的,或者从别处获得?
  • 这些资产是如何集成在一起的?
  • 还需要什么样的集成?
  • 假设已经有了现有资产和那些需要被创建的资产,我们还需要做些什么?
  • 核心域和那些支撑项目的成功几率如何?会不会出现由于其中一个失败而导致整个项目失败的可能?
  • 在哪些地方我们使用了完全不同的术语?
  • 限界上下文之间在哪些地方存在概念重叠?
  • 这些重叠的概念在不同的限界上下文之间是如何映射和翻译的?
  • 哪些限界上下文包含了核心域中的概念,其中使用了哪些战术模式?

  请记住,开发核心域的解决方案是一种关键性的业务投入。

 

  理解限界上下文

  限界上下文是一个显式的边界,领域模型便存在于这个边界之内。领域模型把通用语言表达成软件模型。创建边界的原因在于,每一个模型概念,包括它的属性和操作,在边界之内都具有特殊的含义。如果你是建模团队的一员,你便应该知道这些概念的确切含义。

  在很多情况下,在不同模型中存在名字相同或相近的对象,但是它们的意思却是不同。当模型被一个显式的边界所包围时,其中每个概念的含义便是确定的了。因此,限界上下文主要是一个语义上的边界,我们应该通过这一点来衡量对一个限界上下文的使用正确与否。

  当需要集成时,我们必须在不同的限界上下文之间进行概念映射。在DDD中,这可能是复杂的,因此我们应该特别留意。在上下文边界之外,我们通常不会使用该上下文之内的对象实例,但是不同上下文中彼此关联的对象可能共享一些状态。

  通常情况下,你是可以识别出那些概念分离正确的情况,因为有些相似的对象拥有不同的属性和行为,此时我们可以认为上下文边界的划分是合理的。然而,如果你在不同的限界上下文中看到了完全相同的对象,这通常意味着你的模型是错误的,除非这些限界上下文使用了共享内核。

 

  限界上下文不仅仅只包含模型

  一个限界上下文并不是只包含领域模型。诚然,模型是限界上下文的主要“公民”。但是,限界上下文并不只是局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。有时,限界上下文所包含的内容可能很少,比如,一个通用子域便可以只包含领域模型。

  如果用户界面被用于渲染模型,并且驱动着模型的行为设计时,同样,该用户界面也应该属于模型所在的上下文边界之内。但是,这并不表示我们应该在用户界面中对领域进行建模,因为这样会导致贫血领域对象。我们应该拒绝使用智能UI反模式,或者任何试图将领域概念带到领域模型之外的举措。

  通常情况下,一个系统/应用程序的使用者并不只是人,还可能是另外的计算机系统。系统中有可能存在诸如Web服务之类的组件。我们也可以使用REST资源来与模型交互,此时的REST资源即被称为开放主机服务(Open Host Service)。或者,我们可以使用SOAP或消息服务端点。在以上所有情况中,那些面向服务的组件都应该位于上下文边界之内。

  用户界面或面向服务端点都会将操作委派给应用服务。应用服务包含了不同类型的服务,比如安全和事务管理等。对于模型来说,应用服务扮演的是一种门面模式。同时,应用服务还具有任务管理功能,它将来自用例流的请求转换成领域逻辑的执行流。应用服务也是位于上下文边界之内的。

  限界上下文主要用来封装通用语言和领域对象,但同时它也包含那些为领域模型提供交互手段和辅助功能的内容。需要注意的是,对于架构中的每个组件,我们都应该将其放在适当的地方。

  

  限界上下文的大小

  限界上下文应该足够大,以能够表达它对应的整套通用语言。

  核心领域之外的概念不应该包含在限界上下文之中。如果一个概念不属于你的通用语言,那么一开始你就不应该将其引入到模型中。此外,如果有外部概念进入了你的限界上下文,需要将其清楚,它们可能属于另外的支撑或者通用子域,或者根本就不属于某个模型。

  请注意,不要将本应该属于核心域的概念给清楚掉了。你的模型应该能够完全地展现上下文中的通用语言,而不能遗漏任何重要的概念。此时,我们需要做出正确的判断,可以借助诸如上下文映射图这样的工具。

  使用限界上下文和上下文映射图这样的工具可以帮助我们分析出哪些概念的确应该属于核心域。我们并不随意地采用非DDD的分离原则。

 

  哪些因素会导致我们创建大小不正确的限界上下文呢?

  1. 我们可能错误地采用架构来指导开发设计,而不是通用语言。一些平台、框架或者基础设施通常是用来打包和部署组件的,它们可能会影响我们对限界上下文的设计,此时我们会从技术层面而不是语义边界来考虑问题。
  2. 另一个可能的陷阱是:根据开发任务的分配来拆分限界上下文。技术带头人和项目经理可能会想,小规模任务对于开发者来说将更加容易。这可能是有道理的,但是,为了分配任务而拆分限界上下文是一种错误的上下文建模方式。事实上,我们没有必要为了管理技术资源而创建一些假的上下文边界。
  3. 这里有一个重要的问题:领域专家所采用的语言是如何规划上下文的边界的?

  如果创建限界上下文只是为了架构组件或开发者资源这样的考虑,那么此时的通用语言将变得四分五裂,其表达能力也会丧失殆尽。因此我们应该考虑领域专家所讲的通用语言,将核心域中的概念自然地组织成单一的限界上下文。这样一来,你便可以自然地识别出那些单一的、内聚的模型组件。

 

  有时,我们可以使用模块来避免创建一些微小的限界上下文。通过分析分散在不同限界上下文中的服务,你可能会发现,模块可以将多个限界上下文减少到一个。模块也可以用来拆分开发者的任务职责,因此我们可以使用更加战术化的手段来管理团队的分配任务。

 

  如果你没有采用语言驱动,那么你就不算在和领域专家一起工作来创建限界上下文。认真考虑一下限界上下文的大小,不要急于将其小型化。

 

posted @ 2023-11-22 06:54  Ruby_Lu  阅读(671)  评论(0编辑  收藏  举报