领域驱动模型(DDD)

作者: 高广超 | 来源:发表于2018-03-16 21:32 被阅读379次

    本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。

    什么是领域驱动模型?

    2004年Eric Evans 发表《领域驱动设计——软件核心复杂性应对之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),简称Evans DDD,领域驱动设计思想进入软件开发者的视野。领域驱动设计分为两个阶段:

    • 1、以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;

    • 2、由领域模型驱动软件设计,用代码来实现该领域模型;

    简单地说,软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前,通常需要进行大量的业务知识梳理,而后到达软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念。而领域驱动设计的核心就在于建立正确的领域驱动模型。

    image.png

    传统软件开发与贫血模型

    传统的开发思想

    传统开发四层架构

    image.png

    在传统模型中,对象是数据的载体,只有简单的getter/setter方法,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。

    以商家活动为例,首先设计数据库表配置

    image.png

    设计WmActPoi对象,只有简单的get和set属性方法

    public class WmActPoi {
     
        private Long id;
     
        private String wmPoiId;
     
        private Integer startTime;
     
        private Integer endTime;
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
     
        public Integer getWmPoiId() {
            return wmPoiId;
        }
     
        public WmActPoiDB setWmPoiId(Integer wmPoiId) {
            this.wmPoiId = wmPoiId;
        }
        ......
    }
    

    Service层代码实现

    class WmActPoiService {
        saveWmActPoi();   //保存活动
        checkWmActPoi();   //活动校验
        ....
    }
      
    class WmActPoiQueryService {
        queryWmActPoi();   //查询活动
        queryWmActPoiByWmPoiId();   //根据门店查询活动
        ....
    }
    

    可以看到,业务逻辑都是写在Service中的,WmActPoi充其量只是个数据载体,没有任何行为,是一种贫血模型。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症

    image.png

    传统架构的特点:

    • a. 以数据库为中心

    • b. 贫血模型

    • c. 业务逻辑散落在大量的方法中

    • d. 当系统越来越复杂时,开发时间指数增长,维护成本很高

    DDD设计思想

    采用DDD的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象(类)组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。

    建立领域知识(Build Domain Model)

    说了这么多领域模型的概念,到底什么是领域模型呢?作为一个软件开发者,我们很难在对一个领域不了解的情况下着手开发,所以我们首先需要和领域专家沟通,建立领域只是。以飞机航行为例子:

    现要为航空公司开发一款能够为飞机提供导航,保证无路线冲突监控软件。那我们应该从哪里开始下手呢?根据DDD的思路,我们第一步是建立领域知识:作为平时管理和维护机场飞行秩序的工作人员来说,他们自然就是这个领域的专家,我们第一个目标就是与他们沟通,也许我们并不能从中获取所有想要的知识,但至少可以筛选出主要的内容和元素。你可能会听到诸如起飞,着陆,飞行冲突,延误等领域名词,让们从一个简单的例子开始:

    • 起点->飞机->终点

    这个模型很直接,但有点过于简单,因为我们无法看出飞机在空中做了什么,也无法得知飞机怎么从起点到的终点,那么如此似乎会好些:

    • 飞机->路线->起点/终点

      既然点构成线,那何不:

    • 飞机->路线->points(含起点,终点)

      这个过程,是我们不断建立领域知识的过程,其中的重点就是寻找领域专家频繁沟通,从中提炼必要领域元素。

    通用语言(Ubiquitous Language)

    上面的例子的确看起来简单,但过程并非容易:我们(开发人员)和领域专家在沟通的过程中是存在天然屏障的:我们满脑子都是类,方法,设计模式,算法,继承,封装,多态,如何面向对象等等;这些领域专家是不懂的,他们只知道飞机故障,经纬度,航班路线等专业术语。

    所以,在建立领域知识的时候,我们(开发人员和领域专家)必须要交换知识,知识的范围范围涉及领域模型的各个元素,如果一方对模型的描述令对方感到困惑,那么应该立刻换一种描述方式,直到双方都能够接受并且理解为止。在这一过程中,就需要建立一种通用语言,作为开发人员和领域专家的沟通桥梁。

    可如何形成这种通用语言呢?其实答案并不唯一,确切的说也没有什么标准答案。

    (a) UML

    利用UML可以清晰的表现类,并且展示它们之间的关系。但是一旦聚合关系复杂,UML叶子节点将会变的十分庞大,可能就没有那么直观易懂了。最重要的是,它无法精确的描述类的行为。为了弥补这种缺陷,可以为具体的行为部分补充必要说明(可以是标签或者文档),但这往往又很耗时,而且更新维护起来十分不便。

    (b) 文档/绘图

    文档耗时很长,可能不久就要变化,为模型从一开 始到它达到比较稳定的状态会发生很多次变化, 可能在完成之前它们就已经作废了。对于复杂系统,绘图容易混乱。

    (c) 伪代码

    极限编程推荐这么做,但是使用难度大

    领域驱动设计的分层架构和构成要素

    image.png

    一个通用领域驱动设计的架构性解决方案包含4 个概念层:

    image.png image.png

    层结构的划分是很有必要的,只有清晰的结构,那么最终的领域设计才宜用,比如用户要预定航班,向Application Layer的service发起请求,而后Domain Layler从Infrastructure Layer获取领域对象,校验通过后会更新用户状态,最后再次通过Infratructure Layer持久化到数据库中。

    领域驱动模型的一些要素

    image.png

    实体(Entity) & 值对象(Value Object)

    实体与面向对象中的概念类似,在这里再次提出是因为它是领域模型的基本元素。在领域模型中,实体应该具有唯一的标识符,从设计的一开始就应该考虑实体,决定是否建立一个实体也是十分重要的。

    值对象和我们说的编程中数值类型的变量是不同的,它仅仅是没有唯一标识符的实体,比如有两个收获地址的信息完全一样,那它就是值对象,并不是实体。值对象在领域模型中是可以被共享的,他们应该是“不可变的”(只读的),当有其他地方需要用到值对象时,可以将它的副本作为参数传递。

    服务(Services)

    当我们在分析某一领域时,一直在尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务.

    服务是无状态的,对象是有状态的。所谓状态,就是对象的基本属性:高矮胖瘦,年轻漂亮。服务本身也是对象,但它却没有属性(只有行为),因此说是无状态的。

    服务存在的目的就是为领域提供简单的方法。为了提供大量便捷的方法,自然要关联许多领域模型,所以说,行为(Action)天生就应该存在于服务中。

    服务具有以下特点:

    • a)服务中体现的行为一定是不属于任何实体和值对象的,但它属于领域模型的范围内
    • b)服务的行为一定涉及其他多个对象
    • c)服务的操作是无状态的

    模块(Moudles)

    对于一个复杂的应用来说,领域模型将会变的越来越大,以至于很难去描述和理解,更别提模型之间的关系了。模块的出现,就是为了组织统一的模型概念来达到减少复杂性的目的。而另一个原因则是模块可以提高代码质量和可维护性,比如我们常说的高内聚,低耦合就是要提倡将相关的类内聚在一起实现模块化。

    模块应当有对外的统一接口供其他模块调用,比如有三个对象在模块a中,那么模块b不应该直接操作这三个对象,而是操作暴露的接口。模块的命名也很有讲究,最好能够深层次反映领域模型。

    聚合(Aggregates)

    聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他Entity都有本地表示,但这些标识只有在聚合内部才需要加以区别,因为外部对象除了根Entity之外看不到其他对象。

    image.png

    工厂(Factory)

    一个对象的创建可能是它自身的主要操作,但是复杂的组装操作不应该成为被创建对象的职责。组合这样的职责会产生笨拙的设计, 也很难让人理解。因此,有必要引入一个新的概念,这个概念可以帮助封装复杂的对象创建过程,它就是工厂(Factory)。工厂用来封装对象创建所必需的知识,它们对创建聚合特别有用。当聚合的根建立时,所有聚合包含的对象将随之建立。

    image.png

    资源库(Repositories)

    资源库的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。

    资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。

    Repository的接口应当采用领域通用语言。作为客户端,不应当知道数据库实现的细节。

    Repository和DAO的作用类似,二者的主要区别:

    DAO是比Repository更低的一层,包含了如何从数据库中提取数据的代码。

    Repository以“领域”为中心,所描述的是“领域语言”。Repository把ORM框架与领域模型隔离,对外隐藏封装了数据访问机制。

    image.png
    public
    interface AccountRepository {
        Account findAccount(String accountId);
        void addAccount(Accountaccount);
    }
    

    工厂和资源库之间存在一定的关系。它们都是模型驱动设计中的模式,它们都能帮助我们关联领域对象的生命周期。然而工厂关注的是对象的创建,而资源库关心的是已经存在的对象。资源库可能会 在本地缓存对象,但更常见的情况是需要从一个持久化存储中检索 它们。对象可以用构造函数创建,也可以被传递给一个工厂来构 建。从这个原因上讲,资源库也可以被看作一个工厂,因为它创建对象。不过它不是从无到有创建新的对象,而是对已有对象的重建。我们将不把资源库视为一个工厂。工厂创建新的对象,而资源库应该是用来发现已经创建过的对象。当一个新对象被添加到资源库时,它应该是先由工厂创建过的,然后它应该被传递给资源库以便将来保存它,见下面的例子:

    image.png

    持续集成与模型一致性

    规约(Factory)

    规约是一种布尔断言。


    image.png

    规约是业务规则的 部分 理论上规约类中的方法只有个:isSatisfiedBy(Object obj)。

    规约用来测试对象是否满足某种条件,用来进行对象查询,也可以作为某个对象的创建条件。

    单一规约规则。多个规约通过组合表现复杂的规约。

    image.png image.png image.png image.png

    限界上下文(Bounded Context)

    明确的定义模型所应用的上下文。根据团队的组织、软件系统的功能和物理表现(代码数据库)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的混淆。每个团队负责自己的模型,并为其他模型提供服务。

    上下文映射(Context Map)

    一个企业应用有多个模型,每个模型有自己的界定的上下文。建议用上下文作为团队组织的基础。在同一个团队里的人们能更容易地 沟通,也能很好地将模型集成和实现。但是每个团队都工作于自己 的模型,所以最好让每个人都能了解所有的模型。上下文映射(Context Map)是指抽象出不同界定上下文和它们之间关系的文 档,它可以是像下面所说的一个试图(Diagram),也可以是其他任 何写就的文档。详细的层次各有不同。它的重要之处是让每个在项 目中工作的人都能够得到并理解它。

    image.png

    共享内核(Shared Kernel)

    image.png

    总结

    领域驱动设计的核心是领域模型,这一方法论可以通俗的理解为先找到业务中的领域模型,以领域模型为中心驱动项目的开发。而领域模型的设计精髓在于面向对象分析,在于对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。

    在面向对象编程中讲究封装,讲究设计低耦合,高内聚的类。而对于一个软件工程来讲,仅仅只靠类的设计是不够的,我们需要把紧密联系在一起的业务设计为一个领域模型,让领域模型内部隐藏一些细节,这样一来领域模型和领域模型之间的关系就会变得简单。这一思想有效的降低了复杂的业务之间千丝万缕的耦合关系。

    DDD开发案例

    超市收银业务

    领域驱动设计在互联网业务开发中的实践

    本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。


    欢迎关注 高广超的简书博客 与 收藏文章 !
    欢迎关注 头条号:互联网技术栈

    个人介绍:

    高广超:多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能、可扩展的互联网架构。

    本文首发在 高广超的简书博客 转载请注明!

    简书博客 头条号

    相关文章

      网友评论

      本文标题:领域驱动模型(DDD)

      本文链接:https://www.haomeiwen.com/subject/xvurqftx.html