DDD领域驱动开发概念介绍及简单示例

2022-03-09 11:47:41 浏览数 (1)

什么是领域驱动模型?

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

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

在传统模型中,对象是数据的载体,只有简单的getter/setter方法,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。业务逻辑都是写在Service中的,对象充其量只是个数据载体,没有任何行为,是一种贫血模型。

传统架构的特点:

  • a. 以数据库为中心
  • b. 贫血模型
  • c. 业务逻辑散落在大量的方法中
  • d. 当系统越来越复杂时,开发时间指数增长,维护成本很高

领域驱动模型的一些要素

实体(Entity)

与面向对象中的概念类似,在这里再次提出是因为它是领域模型的基本元素。在领域模型中,实体应该具有唯一的标识符

从设计的一开始就应该考虑实体,决定是否建立一个实体也是十分重要的。另外,不应该给实体定义太多的属性或行为,而应该寻找关联,发现其他一些实体或值对象,将属性或行为转移到其他关联的实体或值对象上。

比如Customer实体,他有一些地址信息,由于地址信息是一个完整的有业务含义的概念,所以,我们可以定义一个Address对象,然后把Customer的地址相关的信息转移到Address对象上。如果没有Address对象,而把这些地址信息直接放在Customer对象上,并且如果对于一些其他的类似Address的信息也都直接放在Customer上,会导致Customer对象很混乱,结构不清晰,最终导致它难以维护和理解;

值对象(Value Object)

与编程中数值类型的变量是不同的,它仅仅是没有唯一标识符的实体,如果两个对象的所有的属性的值都相同我们会认为它们是同一个对象的话,那么我们就可以把这种对象设计为值对象。值对象在领域模型中是可以被共享的,他们应该是“不可变的”(只读的),当有其他地方需要用到值对象时,可以将它的副本作为参数传递。当共享值对象时,一般有复制和共享两种做法。

如果有两个Customer的地址信息是一样的,我们就会认为这两个Customer的地址是同一个。

领域服务(Domain Service)

领域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,因为它们本质上就是一些操作,一些动作,而不是事物。这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。但是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。所以我们需要寻找一种新的模式来表示这种跨多个对象的操作,DDD认为服务是一个很自然的范式用来对应这种跨多个对象的操作,所以就有了领域服务这个模式。

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

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

服务具有以下特点:

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

领域服务存在的意义就是协调领域对象共完成某个操作,所有的状态还是都保存在相应的领域对象中。模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。

聚合及聚合根(Aggregate,Aggregate Root)

聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。

举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。

聚合有以下一些特点:

  1. 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,聚合根是聚合内的某个实体;
  2. 在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他Entity都有本地表示,但这些标识只有在聚合内部才需要加以区别,因为外部对象除了根Entity之外看不到其他对象;
  3. 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;
  4. 聚合根负责与外部其他对象打交道并维护自己内部的业务规则;
  5. 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象;
  6. 聚合内部的对象可以保持对其他聚合根的引用;
  7. 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;

如何识别聚合?

所谓关系是内聚的,是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当我们在修改一个聚合时,我们必须在事务级别确保整个聚合内的所有对象满足这个固定规则。

如何识别聚合根?

  1. 有独立存在的意义,即它是不依赖于其他对象的存在它才有意义的;
  2. 可以被独立访问的,还是必须通过某个其他对象导航得到的;

工厂(Factory)

有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。正如对象封装了内部实现一样(我们无需知道对象的内部实现就可以使用对象的行为),工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,工厂的作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。

工厂在创建一个复杂的领域对象时,通常会知道该满足什么业务规则(它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节),如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象;但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。当然我们也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,所以我们只需要简单的使用构造函数创建对象就可以了。隐藏创建对象的好处是显而易见的,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。

仓库/资源库(Repositories)

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

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

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

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

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

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

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

为什么建立一个领域模型是重要的

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

领域驱动设计告诉我们,在通过软件实现一个业务系统时,建立一个领域模型是非常重要和必要的,因为领域模型具有以下特点:

  1. 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反应了我们在领域内所关注的部分;
  2. 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等;
  3. 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护性,业务可理解性以及可重用性方面都有很好的帮助;
  4. 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造;
  5. 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,所以可以防止需求走样,可以让软件设计开发人员做出来的软件真正满足需求;
  6. 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使大家对领域的认识不断深入,从而不断细化和完善领域模型;
  7. 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是唯一的表达方式,代码或文字描述也能表达领域模型;
  8. 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化;

示例

一、业务分析

大家都去超市买过东西,对超市收银业务都比较熟悉。什么?你不熟?好吧,那我们找个收银员给大家讲解下(领域专家)。

收银员小慧:哦,是这样呢。顾客排队银帐我就收银呢,我要使用收银机呢。收银机就能计算出要收的钱呢,我就扫一下呢,就OK了呢。然后就收银了呢。

听了小慧的讲解,我们心中有了业务的概念了。我们这里采用《业务关键字分析法》来找出此业务流程里面的一些关系字:

  • 商品
  • 顾客
  • 收银员
  • 收银机
  • *收银
  • *选商品
  • *收银员使用收银机
  • *收银机扫商品计算金额

好了,列出这些“业务关键字”了,我们就可以建我们的对象模型了。

二、系统建模

上面我们分析出了一些“业务关键字”接下来我们分析这些业务关键字并深入他们的业务。

商品对象(Goods)

属性:商品名称(GoodsName)、商品价格(GoodsPrice)。

行为:在这里商品对象是没有行为的,我们也可以叫它“值对象”。

顾客对象(Customer)

属性:顾客姓名(CustomerName)、顾客选购的商品(Goodss)

行为:选购想买的商品(LikeBuy)、听收银员说要收多少RMB(ListenAmount)

收银员对象(Cashier)

属性:收银员姓名(CashierName)

行为:收银(CashierRegister)

收银机对象(CashierRegister)

属性:收银机编号(CashRegisterNo)

字段:总金额(_totalAmount)

行为:收银(CashRegisters)、显示收银总额(ShowAmount)

有木有很直观,这也就是面向对象分析的好处,因为对象就是对现实的抽象,我们现实中的事务可以很方便的用对象抽象出来。我们很容易发现,这和用表来描述这些业务模型显然要不方便的多。表还只能描述属性,造成了属性与行为的分离。

三、代码示例

商品对象

代码语言:javascript复制
/// <summary>
    /// 商品
    /// </summary>
    public class Goods
    {
        /// <summary>
        /// 对象标识
        /// </summary>
        public Guid OKey { get; set; }
        /// <summary>
        /// 商品名称
        /// </summary>
        public string GoodsName { get; set; }
        /// <summary>
        /// 商品价格
        /// </summary>
        public decimal GoodsPrice { get; set; }
    }

顾客对象

代码语言:javascript复制
/// <summary>
    /// 顾客
    /// </summary>
    public class Customer
    {
        /// <summary>
        /// 对象标识
        /// </summary>
        public Guid OKey { get; set; }
        /// <summary>
        /// 顾客姓名
        /// </summary>
        public string CustomerName { get; set; }

        private List<Goods> _goodss = new List<Goods>();
        /// <summary>
        /// 顾客购买的商品
        /// </summary>
        public List<Goods> Goodss
        {
            get { return _goodss; }
            set { _goodss = value; }
        }

        /// <summary>
        /// 顾客选购商品
        /// </summary>
        /// <param name="goods">商品</param>
        public void LikeBuy(Goods goods)
        {
            this._goodss.Add(goods);
        }

        /// <summary>
        /// 听收银员应收多少钱
        /// </summary>
        /// <param name="amount"></param>
        public void ListenAmount(decimal amount)
        {
            Console.WriteLine("我是[{0}],我买了{1}件商品。我共花了{2}元RMB。", this.CustomerName, this.Goodss.Count, amount.ToString("f2"));
        }

收银员对象

代码语言:javascript复制
/// <summary>
    /// 收银员
    /// </summary>
    public class Cashier
    {
        /// <summary>
        /// 对象标识
        /// </summary>
        public Guid OKey { get; set; }
        /// <summary>
        /// 收银员姓名
        /// </summary>
        public string CashierName { get; set; }

        /// <summary>
        /// 收银
        /// </summary>
        /// <param name="customer">顾客</param>
        public void CashRegister(Customer customer)
        {
            //打开使用收银机
            CashRegister cashRegister = new CashRegister();

            //对顾客的商品进行收银机扫码,收银
            foreach (var goods in customer.Goodss)
            {
                //使用收银机扫商品进行收银
                cashRegister.CashRegisters(goods);
            }

            //通知顾客一共收多少钱
            customer.ListenAmount(cashRegister.ShowAmount());
        }
    }

收银机对象

代码语言:javascript复制
/// <summary>
        /// 对象标识
        /// </summary>
        public Guid OKey { get; set; }
        /// <summary>
        /// 收银机编号
        /// </summary>
        public string CashRegisterNo { get; set; }

        /// <summary>
        /// 总价格
        /// </summary>
        private decimal _totalAmount { get; set; }

        public CashRegister()
        {
            //收银总额置0
            this._totalAmount = 0;
        }

        /// <summary>
        /// 收银
        /// </summary>
        /// <param name="goods">商品</param>
        public void CashRegisters(Goods goods)
        {
            this._totalAmount  = goods.GoodsPrice;
        }

        /// <summary>
        /// 显示收银总额
        /// </summary>
        /// <returns></returns>
        public decimal ShowAmount()
        {
            return this._totalAmount;
        }

模拟业务流程

代码语言:javascript复制
//我们创建几样商品
            Goods RedWine = new Goods() { GoodsName = "红酒", GoodsPrice = 1800,OKey=Guid.NewGuid() };
            Goods Condoms = new Goods() { GoodsName = "安全套", GoodsPrice = 35,OKey=Guid.NewGuid() };

            //我们创建几位顾客
            Customer Chunge = new Customer() { CustomerName = "春哥", OKey = Guid.NewGuid() };
            Customer Beianqi = new Customer() { CustomerName = "贝安琪", OKey = Guid.NewGuid() };

            //当然,我们需要收银员啊
            Cashier CashierMM = new Cashier() { CashierName = "收银员MM", OKey = Guid.NewGuid() };

            //顾客逛了一圈,选了自己想要的商品
            Chunge.LikeBuy(RedWine);
            Beianqi.LikeBuy(RedWine);
            Beianqi.LikeBuy(Condoms);

            //顾客开始排队结帐了
            Queue<Customer> customerQueue = new Queue<Customer>();
            customerQueue.Enqueue(Chunge);
            customerQueue.Enqueue(Beianqi);

            //队伍过来,按先后顺序挨个收银喽
            foreach (var customer in customerQueue)
            {
                //收银
                CashierMM.CashRegister(customer);
            }

显示结果

参考

领域驱动模型(DDD):https://cloud.tencent.com/developer/article/1371115

DDD领域驱动设计基本理论知识总结:https://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html

领域对象驱动开:https://www.cnblogs.com/assion/archive/2011/05/13/2045253.html

0 人点赞