《实现领域驱动设计》
DDD入门
1.1 DDD是什么?
DDD是一种软件开发方法
- DDD将领域专家和开发人员聚集到一起,开发的软件能够反映出领域专家的思维模型。目标是:交付最具业务价值的软件。
- DDD关注业务战略:指引我们如何实现面向服务架构(service-oriented architecture)或者业务驱动(business-driven architecture)架构。
- 使用战术设计建模工具:这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。保证成功地交付真正的业务价值。
1.2 为什么我们需要DDD?
- 使领域专家和开发者在一起工作,这样开发的软件能够准确地传达业务规则。
- "准确传达业务规则"意思是说:此时的软件就像是领域专家是编码人员时所开发出来的一样。
- 可以帮助业务人员自我提高。在DDD中,每个人都在学习,同时每个人又是知识的贡献者。
- 关键在于对于知识的集中,确保软件知识并不只是掌握在少数人手中。
- 在领域专家、开发者和软件本身之间不存在"翻译"
- 设计就是代码,代码就是设计
- DDD提供了战略设计和战术设计两种方式。
战略设计:帮助我们理解哪些投入是最重要的;
战术设计:帮助我们创建DDD模型中各个部件。
1.3 如何DDD?
通用语言、限界上下文(Bounded Context)同时构成了DDD的两大支柱,并且它们是相辅相成的。
通用语言
通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流。事实上,团队中每个人都使用相同的通用语言。不管你再团队中的角色如何,只要你是团队的一员,都将使用通用语言。团队成员通过讨论、参考资料、引用标准、查阅字典等对通用语言进行改进。有时我们发现,有些我们曾经认为能很好表达业务的词汇不再适用了,而另外的一些词汇具有更好的效果。
1.4 使用DDD的业务价值
不管使用什么技术,我们的目的都是提供业务价值。
我们可以将DDD的业务价值大致总结为以下几点:
1.你获得了一个非常有用的领域模型
2.你的业务得到了更准确的定义和理解
3.领域专家可以为软件设计做出贡献
4.更好的用户体验
5.清晰的模型边界
6.更好的企业架构
7.敏捷、选代式和持续建模
8.使用战略和战术新工具
1.5 实施DDD所面临的挑战
- 为创建通用语言腾出时间和精力
使用DDD最大的挑战之一便是:我们需要花费大量的时间和精力来思考业务领域,研究概念和术语,并且和领域专家交流,以发现、捕捉和改进通用语言。如果你想完全采用DDD来最大化业务价值,你需要做出很多努力,并且花费很多时间。 - 持续地将领域专家引入项目
要将领域专家引入你的项目恐怕也不是一件易事。但是不管有多么困难,这是你必须做的。如果你连一个领域专家都找不到,那么你根本无法对一个领域有深入的理解。 - 改变开发者对领域的思考方式
多数开发者在采用DDD时都需要转变自己思考问题的方式。作为开发者,我们都是技术思想者,技术实现对于我们来说并不是什么难事。我并不是说技术地思考不好,只是说有时少从技术层面去思考会更好。这么多年来,我们都习惯了单从技术层面完成软件开发,那么现在,是时候考虑一种新的思考方式了。为你的业务领域开发一门通用语言便是一个好的出发点。
2 领域、子域、界限上下文
由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建一个单一的、内聚的、全功能式的模型。然而,这并不是我们使用DDD的目标。正好相反,在DDD中,一个领域被分为若干子域,领域模型在限界上下文中完成开发。事实上,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。
工作中的子域和限界上下文
将关注点放在核心域上
图2.2上半部分的领域边界,你会看到一个叫核心域的子域。
对于核心域,它是整个业务领域的一部分,也是业务成功的主要促成因素。从战略层面上讲,企业应该在核心域上胜人一筹。我们应该给予核心域最高的优先级、最资深的领域专家和最优秀的开发团队。在实施DDD的过程中,你将主要关注于核心域。
图2.2中还展示了另外两种子域:支撑子域和通用子域。
有时,我们会创建或者购买某个限界上下文来支撑我们的业务。如果这样的限界上下文对应着业务的某些重要方面,但却不是核心,那么它便是一个支撑子域。
创建支撑子域的原因在于它们专注于业务的某个方面,否则,如果一个子域被用于整个业务系统,那么这个子域便是通用子域。
我们并不能说支撑子域和通用子域是不重要的,它们是重要的,只是我们对它们的要求并不像核心域那么高。
2.1 战略设计为什么重要
2.2 现实世界中领域和子域
领域中还同时存在问题空间(problem space)和解决方案空间(solution space)在问题空间中,我们思考的是业务所面临的挑战,而在解决方案空间中,我们思考如何实现软件以解决这些业务挑战。
以下是如何将这两者应用到我们已经学过的知识中:
- 问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。
对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速地浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。 - 解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。
这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。
2.3 理解限界上下文
在很多情况下在不同模型中存在名字相同或相近的对象,但是它们的意思却不同。当模型被一个显式的边界所包围时,其中每个概念的含义便是确定的了。因此,限界上下文主要是一个语义上的边界,我们应该通过这一点来衡量对一个限界上下文的使用正确与否。
不同限界上下文中的Account对象具有完全不同的含义
银行上下文:账户表示一个客户在银行的存款支票账户和储蓄账户
状态,并记录每次交易信息。
文学上下文:账户表示用文字记录的在一段时间之内发生的一系列事件。
在通常情况下,我们所面对的都是一些区别甚小的概念定义。原因在于:在一个上下文中,团队通常根据通用语言来命名某个概念。我们并不会随意地命名一个概念以刻意地保持与其他上下文的不同。
比如两个银行上下文,一个用于支票账户另一个用于储蓄账户。在支票上下文(Checking
Context)中我们不必使用支票账户(CheckingAccount);在储蓄上下文(Saving Context)中我们也不必使用储蓄账户(SavingAccount)。两个概念都可以使用账户(Account)来表示因为限界上下文已经对此做了区分。当然,我们并没有规定不能使用更具体的名字这只是团队自己的选择而已。
当需要集成时,我们必须在不同的限界上下文之间进行概念映射。
限界上下文不仅仅只包含模型
一个限界上下文并不是只包含领域模型。诚然,模型是限界上下文的主要“公民”。但是,限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务”。有时,限界上下文所包含的内容可能比较少,比如,一个通用子域便可以只包含领域模型。
限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。需要注意的是,对于架构中的每个组件,我们都应该将其放在适当的地方。
限界上下文的大小
限界上下文应该足够大,以能够表达它所对应的整套通用语言。
核心领域之外的概念不应该包含在限界上下文中。如果一个概念不属于你的通用语言,那么一开始你就不应该将其引入到模型中。
与技术组件保持一致
将限界上下文想成是技术组件并无大碍,只是我们需要记住:技术组件并不能定义限界上下文。
让我们来看看示例DDD项目中的这3个限界上下文。他们分别是协作上下文、身份与访问上下文和敏捷项目管理上下文。
协作上下文
协作上下文的团队将所有与安全和权限相关的模块和类型从该上下文中移除,然后逐渐采用新的身份与访问上下文。
身份与访问上下文
现在,多数企业级应用程序都需要某种形式的安全和权限组件,这样的组件用以对用户进行认证和授权。正如我们在前面所分析的,一种幼稚的做法是将这样的组件嵌入到每一个离散的系统中,这将导致每一个系统都产生筒仓效应(siloeffect) .
当有我们关心的状态由于模型行为而发生改变时,系统将发布领域事件(8)。
敏捷项目管理上下文
3 上下文映射图
一个项目的上下文映射图(Context Map)可以用两种方式来表示比较容易的一种是画一个简单的框图来表示两个或多个限界上下文之间的映射关系。该框图表示了不同的限界上下文在解决方案空间中是如何通过集成相互关联的。另一种更详细的方式是通过限界上下文集成的源代码实现来表示。
3.1 上下文映射图为什么重要
你的映射图可以使你了解到映射图的内部,并且可以指明在哪些地方需要与其他团队进行交流。尽早绘制上下文映射图,这样可以迫使你仔细思考你的项目和你所依赖项目之间的关系。
绘制上下文映射图
上下文映射图并不是一种企业架构,也不是系统拓扑图。但是,它可以用于高层次的架构分析,指出诸如集成瓶颈之类的架构不足。上下文映射图展现了一种组织动态能力(organizational dynamic)它可以帮助我们识别出有碍项目进展的一些管理问题。
限界上下文之间的关系
- 合作关系(Partnership)
- 共享内核(SharedKernel)
对模型和代码的共享将产生一种紧密的依赖性。
对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。 - 客户方-供应方开发(Customer-Supplier Development)
当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。 - 遵奉者(Conformist)
在存在上游-下游关系的两个团队中如果上游团队已经没有动力提供下游团队之所需,下游团队便孤军无助了。 - 防腐层(Anticorruption Layer)
防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间进行翻译转换。 - 开放主机服务(Open Host Service)
定义一种协议,让你的子系统通过该协议来访问你的服务。 - 发布语言(Published Language)
在两个限界上下文之间翻译模型需要一种公用的语言。 - 另谋他路(SeparateWay)
在确定需求时我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。 - 大泥球(Big Ballof Mud)
当我们检查已有系统时经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。
映射3个示例限界上下文
身份与访问上下文位于最上游它对协作上下文和敏捷项目管理上下文均会产生影响。同时,协作上下文又是敏捷项目管理上下文的上游,因为后者的模型依赖于前者的模型和服务。
所有下游系统的连接框都标以ACL,即防腐层。对于它们的技术实现将在集成限界上下文(13)中讲到。
协作上下文
协作上下文通过传统的类似于RPC的方式获取外部资源。协作上下文并不会永久性地记录下从身份与访问上下文中获取来的数据而是在每次需要数据时重新向远程系统发出请求。
敏捷项目管理上下文
为了达到比RPC更高的自治性,敏捷项目管理上下文的团队将尽量限制对RPC的使用,此时他们可以选择异步请求,或者事件处理等方式。
DDD的做法是:在本地创建一些由外部模型翻译而成的领域对象,这些对象保留着本地模型所需的最小状态集。为了初始化这些对象,我们只需要有限的RPC调用或REST请求。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布也可以采用消息队列或者REST。
和身份与访问上下文集成
敏捷项目管理上下文在本地有DiscussionService和SchedulingService,它们是领域服务,用于管理协作系统中的讨论和日历条目
4 架构
DDD的一大好处便是它并不需要使用特定的架构。由于核心域(2)位于限界上下文(2)中我们可以在整个系统中使用多种风格的架构有些架构包围着领域模型,能够全局性地影响系统,而有些架构则满足了某些特定的需求。我们的目标是选择适合于自己的架构和架构模式。
4.2 分层
分层架构模式[Buschmannetal被认为是所有架构的始祖它支持N层架构系统,因此被广泛地应用于Web、企业级应用和桌面应用。在这种架构中,我们将一个应用程序或者系统分为不同的层次。
图4.1所示为一个典型的DDD系统所采用的传统分层架构,其中核心域只位于架构中的其中一层其上为用户界面层(User Interface)和应用层(ApplicationLayer),其下是基础设施层(Infrastructure Layer)。
4.3 六边形架构 (端口与适配器)
4.4 面向服务架构SOA
4.5 REST
4.6 命令和查询职责分离
CORS