首页 > 代码库 > 领域驱动设计 ——一种将概念模型化的方式

领域驱动设计 ——一种将概念模型化的方式

原文发布于:http://www.gufeng.tech/

1.引子

      2004年Eric Evans 发表了一本书:《Domain-Driven Design: Tackling Complexity in the Heart of Software》(中文名:《领域驱动设计:软件核心复杂性应对之道》),在这本书中作者提出了领域驱动设计(DDD)的概念,到现在已经10多年的时间了。

1.1 面向对象与面向对象语言

      面向对象思想已经存在相当长的历史了(相对于软件的历史),我而们使用的语言,很多也都是面向对象的,但是我们使用面向对象的语言就一定能写出来面向对象的程序吗?显然是不可能的。业务逻辑代码的堆积、缺乏良好设计的系统或模块亦或是功能,这样是不能保证代码的复用性、扩展性的。

1.2  领域模型

      领域驱动设计的出现,就是为了解决这一问题的。领域驱动设计是以建立正确的领域模型为核心,以构建清晰的分层架构基础,从而使面向对象的开发进入到了一个新的阶段。

      领域驱动设计的前提是有一种能够在领域专家(业务专家)、设计人员、开发人员(为什么会有开发人员,我们会在后面介绍原因)三类参与者通用的沟通语言,在三类参与者的不断交流、沟通中发现领域概念(业务概念),再将概念固化成模型,最后由领域模型驱动设计并实现。

      说到这里,看上去领域模型并没有什么特别的地方,与我们日常分析的方式没什么大的区别,我们首先来简单介绍写领域模型的两个特点:

      1)业务逻辑集中在领域对象(类)上;

      2)每个领域对象是完整和独立的,并具有自己的属性和行为。

      在接下来的内容中,我们一起来了解下如何实现领域驱动设计以及领域驱动设计的优点。

2.领域驱动设计

      领域驱动设计涵盖了领域模型、领域语言、架构设计、实现几部分内容,下面我们逐一了解一下。

2.1 领域模型

      关于什么是领域模型以及领域模型的特点,在前面内容中我们有了整体的了解,接下来我们就领域模型本身进行一下简单的了解。

2.1.1 抽象模型

      领域模型是某个边界内的领域的一个抽象,是客观世界的模型,首先它使有边界的,清晰的边界是领域模型抽象是否完整的一个重要衡量指标。在该领域模型内,我们只关心领域内的内容。

      领域模型只是实际业务的一种反映,与具体实现技术无关。可以说领域模型建立的成功与否,直接关系到最终的实现、使用等等。领域模型确保任参与人在任何时间看到的内容都是一样的,了解了模型,就能知道实现的步骤。

      领域模型对于提高软件的维护性、复用性以及业务可理解性等方面都有很好的帮助。领域模型贯穿整个分析、设计、开发过程,前面提到的三类参与者使用一种大家都能理解的语言进行沟通,确保所有人对模型的理解是一致的,这样最终开发出来的结果和最初的设计才能最大程度的吻合。

      要建立一个好的领域模型并不简单,甚至可能是一路坎坷,需要领域专家、设计人员、开发人员通力配合、深入交流、共享信息和知识。最后,领域模型要通过文档或图形方式展现出来(推荐使用图形分解整体结构,配以文字说明)。设计足够好的领域模型,肯定是符合业务需求的,同时也能够快速响应需求变化。

      与领域模型紧密相关的还有另外一组概念:聚合、聚合根。下面我们来简单了解下这两个概念。

      聚合:通过定义对象间的隶属关系和边界来实现领域模型的内聚,

      聚合根:聚合内的某个实体,外部调用聚合时,必须从聚合根开始调用,不能绕过。

      关于聚合的一些特点:

      1)每个聚合有一个根和边界;

      2)内部对象可互相引用,但是外部对象访问聚合时,必须从聚合根开始;

      3)除根外,其它对象在聚合内保持唯一即可;

      4)聚合内部对象可以保持对其它聚合根的引用;

      5)删除聚合根时,必须同时删除其它聚合内对象。

      所有具有独立含义并且能够被单独访问的内容是聚合。

2.1.2 领域通用语言

      设想一下,领域专家满口的专业术语,设计人员满口的设计理论,开发人员满口的开发语言及算法,这样的团队怎么沟通!当然可以引入“翻译”,但是“翻译”的结果以及对结果的理解会造成多大程度上的信息丢失,谁也不确定。

      基于以上的原因,迫切需要一种大家都能够表达出来和理解的语言——这就是领域通用语言。领域通用语言是领域驱动设计的基础和前提。在三类参与者的各种形式沟通中,都要使用领域通用语言,确保自己的信息能够被其他人完整、快速的理解。

2.1.3 模型到实现

      假设我们已经拥有了一个非常正确且严谨的模型,那么是否能将这个模型直接转换成代码吗?肯定是不行的。所以要求我们在领域建模和设计时,就要考虑最终的代码实现,将领域模型与实现紧密关联起来,这就是为什么要有开发人员参与的原因。

      这样的结构(开发人员参与模型建立、结构设计)有利于尽早发现那些不适合在软件中实现的模型部分并要求修正,这样也避免了在最后实现时发现问题、修正设计所带来的巨大时间损失。同时,因为开发人员参与了模型设计,所以在编码实现时,都会尽力保护模型不被破坏(因为这是大家共同努力的结果),同时当开发人员发现编码实现有不满足模型或者不完善的地方,也会去完善它,进行代码重构,这样能够在很大程度上提升软件的可靠性,也便于其他人员在接手时能快速了解模型、掌握实现。

2.2 领域驱动设计的架构分层

      我们先来看一张Eric Evans 在他的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中提到的分层图:

技术分享

     关于这张图可能都不陌生,但是每一层在领域驱动设计中的职责是什么?完成什么样的功能?层与层之间的协作关系是什么样的?这些问题会在后面一一解释。

2.2.1 分层

      1)用户界面

      人机交互部分,没有特殊内容。

      2)应用

       此应用非彼应用,这里的应用只是很薄的一层,用于给User Interface提供功能接口,并调用Domain完成功能逻辑,看到这里应该有了比较明确的认识了,Application不包括任何业务,只是将根据User Interface的需要提供接口,并完成对一个或者多个Domain的调用。

       本层包含了所有软件系统要完成的任务,通过本层就能了解整体功能,User Interface仅仅是一种展现方式。

      3)领域

       这一层是整个系统的核心部分,包括了全部的业务逻辑、业务规则等全部业务相关内容。

      4)基础设施

       这里的基础设施指的是基础技术组件,包括消息通信、持久化、缓存等等所有的基础技术组件。

2.2.2 几种辅助模式

      1)实体(Entity)

      具有跨越系统的生命周期甚至能超越软件系统的一系列的延续性和标识符的对象成为实体。简单说就是具有绝对唯一标识的对象。比如银行账户的ID是唯一的标识,那么一个银行账户就是一个实体。实体拥有自己的属性,管理自己的内部状态并对外暴露行为。

      2)值对象(Value Object)

      当我们关心对象的唯一标识而只关心其属性值的时候,这个对象就是一个值对象。对于值对象,理论上可以被轻易的创建以丢掉。如果是可共享值对象,那应该确保它的值是不可变的。“值对象应该保持尽量的简单。当其他当事人需要一个值对象时,可以简单地传递值,或者创建一个副本。”

      3)服务(Service)

      是不是所有的领域都能映射成对象呢?显然是不可能的,那么如何处理不能够映射成对象的领域呢?这时候就需要服务这个东西了。服务通常对应领域的动作,代表领域中得一些重要行为,而这些行为又不属于任何一个实体或者值对象。这些行为可以定义为服务对象。

      服务可能存在于领域层、基础设施层等,所以要区分服务,不要滥用服务。

      服务对象不包含内部状态,只有行为,所以它提供的主要是行为,作为操作接口存在。我们需要注意的是,不需要对每一个操作创建服务。我们一起来一下服务的几个特征:

    (1)服务执行的操作涉及一个领域概念,这个领域概念通常不属于一个实体或者值对象;

    (2)被执行的操作涉及到领域中的其他的对象;

    (3)操作是无状态的。

      4)模块(Module)

      当模型巨大,难以整体讨论时,需要把这个大得模型拆成几个关联的模块。

      5)聚合&聚合根

      聚合是针对数据变化可以考虑成一个单元的一组相关的对象。聚合使用边界将内部和外部的对象划分开来。每个聚合有一个根,是一个实体,并且它是外部可以访问的唯一的对象。

      关于聚合与聚合根的内容,可参考2.1.1节。

       6)Factory(工厂)

      引入工厂模式,是因为领域模型本身的复杂性决定的,创建领域对象要远远比创建pojo对象复杂得多,尤其是聚合会更加复杂。此时引入工厂模式,可以将复杂的实现放在工厂内,外部调用工厂方法即可得到相应领域对象,同时也隐藏了创建逻辑(主要是提供给Application和Infrastructure使用的)。

       7)Repository(仓储、资源库)

      仓储最初的设计目的是用来管理内存中的对象,但我们可以扩展使用,对于需要持久化的领域对象,使用Repository将其持久化到数据库(或其它持久化存储)中,再次需要时,可以通过Repository将对象从数据库中恢复。通常情况下,一个聚合对应一个仓储。

      那么对于那些不能够通过单一Repository查询出来的结果(比如界面中需要展现的数据来源于多个Repository的情况)我们该怎么办呢?当然可以通过调用多个Repository查询出结果,但更好的方式是通过CQRS架构来实现,也就是说对于查询可绕过Domain,直接由Application发起调用另外的架构或者层来实现。

       8)CQRS(Command Query Responsibility Segregation,命令查询职责分离)

      从字面理解,就是命令和查询要分离开,那么什么事命令呢?非查询的操作即命令。结合领域驱动设计,我们可以理解成命令可以通过领域驱动设计完成,查询则可使用简单、直接的方式完成(如直接写SQL)。

      由于是分离的,所以两部分可以采用相同甚至完全不同的架构来实现,由此引申,是不是数据库也可以分开设计呢?当然是可以的。

3.结束

      本文中我们粗略的了解了领域驱动设计的一些基本概念、原则和一些所谓的“模式”,在实际使用或者叫“领域驱动设计落地”的过程中,除了一些必须遵守的原则外,我们可以根据自己的业务特点、团队优势进行裁剪。

      没有任何一种语言是具有绝对优势的,同样也没有任何一种设计方法是绝对正确的。找准我们自己的方向,找出适合我们业务特点、团队特点的方法,并对该方法进行落地裁剪,使之更具生命力、能够解决我们的实际问题。

      最后,不要迷信、迷恋任何一种或几种方法、模式,所有的方法都是人根据经验总结出来,方法、模式可以参考并综合使用,最终达到拥有自己的方法、自己的模式,这样才能更好的服务于自己的业务,创造技术体系。

本文出自 “谷风” 博客,请务必保留此出处http://1202955.blog.51cto.com/1192955/1920983

领域驱动设计 ——一种将概念模型化的方式