领域驱动设计-相关理论

最近 Rafy 开源中心 启动刚一个月,在初始的讨论会上,成员们对面向对象设计、领域驱动设计等概念展开了大量的讨论。html

下面我转载一篇文章,这篇文章的详细内容我都还没看完。不过,文章的结构正是我想要的!其结构很是清晰,很好地说明了领域驱动设计相关的起源、重点、模式、经典架构,以及一些后人扩展的新概念。程序员

转载自《DDD领域驱动设计基本理论知识总结》,并稍微调整了一下内容顺序。算法


为何面向对象比面向过程更能适应业务变化

对象将需求用类一个个隔开,就像用储物箱把东西一个个封装起来同样,需求变了,分几种状况,最严重的是大变,那么每一个储物箱都要打开改,这种方法就不见得有好处;可是这种状况发生几率比较小,大部分需求变化都是局限在一两个储物箱中,那么咱们只要打开这两个储物箱修改就能够,不会影响其余储物柜了。数据库

而面向过程是把全部东西都放在一个大储物箱中,修改某个部分之后,会引发其余部分不稳定,一个BUG修复,引起新的无数BUG,最后程序员陷入焦头烂额,如日本东京电力公司员工处理核危机同样,心力交瘁啊。编程

因此,咱们不能粗粒度看需求变,认为需求变了,就是大范围变,万事万物都有边界,老子说,无欲观其缴,什么事物都要观察其边界,虽然需求能够用“需求”这个名词表达,谈到需求变了,不都意味着最大边界范围的变化,这样看问题容易走极端。设计模式

其实就是就地画圈圈——边界。咱们小时候写做文分老三段也是一样道理,各自职责明确,划分边界明确,经过过渡句实现承上启下——接口。为何组织须要分不一样部门,一样是边界思惟。画圈圈容易,但如何画才难,因此OO中思惟很是重要。安全

需求变化所引发的变化是有边界,若果变化的边界等于整个领域,那么已是彻底不一样的项目了。要掌握边界,是须要大量的领域知识的。不然,走进银行连业务职责都分不清的,如何画圈圈呢?架构

面向过程是无边界一词的(就算有也只是最大的边界),它没有要求各自独立,它能够横跨边界进行调用,这就是容易引发BUG的缘由,引发BUG不必定是技术错误,更多的是逻辑错误。分别封装就是画圈圈了,全部边界都以接口实现。不用改或者小改接口,都不会牵一发动全身。若果面向过程当中考虑边界,那么也就已经上升到OO思惟,即便用的不是对象语言,但对象已经隐含其中。说白了,面向对象与面向过程最大区别就是:分解。边界的分解。从需求到最后实现都贯穿。框架

面向对象的实质就是边界划分,封装,不但对需求变化可以量化,缩小影响面;由于边界划分也会限制出错的影响范围,因此OO对软件后期BUG等出错也有好处。dom

软件世界永远都有BUG,BUG是清除不干净的,就像人类世界永远都存在不完美和阴暗面,问题关键是:上帝用空间和时间的边界把人类世界痛苦灾难等不完美局限在一个范围内;而软件世界若是你不采起OO等方法进行边界划分的话,一旦出错,追查起来状况会有多糟呢?

软件世界其实相似人类现实世界,有时出问题了,探究缘由一看,原来是两个看上去毫无联系的因素致使的,古人只好常常求神拜佛,咱们程序员在本身的软件上线运行时,大概内心也在求神拜佛别出大纰漏,若是咱们的软件采起OO封装,咱们就会坦然些,确定会出错,可是咱们已经预先划定好边界,因此,不会产生严重后果,甚至也不会出现难以追查的魔鬼BUG。

领域驱动设计之领域模型

加一个导航,关于如何设计聚合的详细思考,见这篇文章。

2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD。领域驱动设计分为两个阶段:

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

因而可知,领域驱动设计的核心是创建正确的领域模型。

为何创建一个领域模型是重要的

领域驱动设计告诉咱们,在经过软件实现一个业务系统时,创建一个领域模型是很是重要和必要的,由于领域模型具备如下特色:

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

领域通用语言(UBIQUITOUS LANGUAGE)

咱们认识到由软件专家和领域专家通力合做开发出一个领域的模型是绝对须要的,可是,那种方法一般会因为一些基础交流的障碍而存在难点。开发人员满脑子都是类、方法、算法、模式、架构,等等,老是想将实际生活中的概念和程序工件进行对应。他们但愿看到要创建哪些对象类,要如何对对象类之间的关系建模。他们会习惯按照封装、继承、多态等面向对象编程中的概念去思考,会随时随地这样交谈,这对他们来讲这太正常不过了,开发人员就是开发人员。可是领域专家一般对这一无所知,他们对软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。好比,在空中交通监控样例中,领域专家知道飞机、路线、海拔、经度、纬度,知道飞机偏离了正常路线,知道飞机的发射。他们用他们本身的术语讨论这些事情,有时这对于外行来讲很难直接理解。若是一我的说了什么事情,其余的人不能理解,或者更糟的是错误理解成其余事情,又有什么机会来保证项目成功呢?

在交流的过程当中,须要作翻译才能让其余的人理解这些概念。开发人员可能会努力使用外行人的语言来解析一些设计模式,但这并必定都能成功奏效。领域专家也可能会建立一种新的行话以努力表达他们的这些想法。在这个痛苦的交流过程当中,这种类型的翻译并不能对知识的构建过程产生帮助。

领域驱动设计的一个核心的原则是使用一种基于模型的语言。由于模型是软件知足领域的共同点,它很适合做为这种通用语言的构造基础。使用模型做为语言的核心骨架,要求团队在进行全部的交流是都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿须要确保团队使用的语言在全部的交流形式中看上去都是一致的,这种语言被称为“通用语言(Ubiquitous Language)”。通用语言应该在建模过程当中普遍尝试以推进软件专家和领域专家之间的沟通,从而发现要在模型中使用的主要的领域概念。

将领域模型转换为代码实现的最佳实践

拥有一个看上去正确的模型不表明模型能被直接转换成代码,也或者它的实现可能会违背某些咱们所不建议的软件设计原则。咱们该如何实现从模型到代码的转换,并让代码具备可扩展性、可维护性,高性能等指标呢?另外,如实反映领域的模型可能会致使对象持久化的一系列问题,或者致使不可接受的性能问题。那么咱们应该怎么作呢?

咱们应该紧密关联领域建模和设计,紧密将领域模型和软件编码实现捆绑在一块儿,模型在构建时就考虑到软件和设计。开发人员会被加入到建模的过程当中来。主要的想法是选择一个可以恰当在软件中表现的模型,这样设计过程会很顺畅而且基于模型。代码和其下的模型紧密关联会让代码更有意义并与模型更相关。有了开发人员的参与就会有反馈。它能保证模型被实现成软件。若是其中某处有错误,会在早期就被标识出来,问题也会容易修正。写代码的人会很好地了解模型,会感受本身有责任保持它的完整性。他们会意识到对代码的一个变动其实就隐含着对模型的变动,另外,若是哪里的代码不能表现原始模型的话,他们会重构代码。若是分析人员从实现过程当中分离出去,他会再也不关心开发过程当中引入的局限性。最终结果是模型再也不实用。任何技术人员想对模型作出贡献必须花费一些时间来接触代码,不管他在项目中担负的是什么主要角色。任何一个负责修改代码的人都必须学会用代码表现模型。每位开发人员都必须参与到必定级别的领域讨论中并和领域专家联络。

领域建模时思考问题的角度

“用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。 《老子》书中有个观点:有之觉得利,无之觉得用。在这里,有之利,即创建领域模型;无之用,即包容用户需求。举些例子,一个杯子要装满一杯水,咱们在制做杯子时,制做的是空杯子,即要把水倒出来,以后才能装下水;再好比,一座房子要住人,咱们在建造房子时,建造的房子是空的,惟有空的才能容纳人的居住。所以,创建领域模型时也要将用户置于模型以外,这样才能包容用户的需求。

因此,个人理解是:

  1. 咱们设计领域模型时不能以用户为中心做为出发点去思考问题,不能总是想着用户会对系统作什么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律做为出发点去思考问题。
  2. 领域模型是排除了人以外的客观世界模型,可是领域模型包含人所扮演的参与者角色,可是通常状况下不要让参与者角色在领域模型中占据主要位置,若是以人所扮演的参与者角色在领域模型中占据主要位置,那么各个系统的领域模型将变得没有差异,由于软件系统就是一我的机交互的系统,都是以人为主的活动记录或跟踪;好比:论坛中若是以人为主导,那么领域模型就是:人发帖,人回帖,人结贴,等等;DDD的例子中,若是是以人为中心的话,就变成了:托运人托运货物,收货人收货物,付款人付款,等等;所以,当咱们谈及领域模型时,已经默认把人的因素排除开了,由于领域只有对人来讲才有意义,人是在领域范围以外的,若是人也划入领域,领域模型将很难保持客观性。领域模型是与谁用和怎样用是无关的客观模型。概括起来讲就是,领域建模是创建虚拟模型让咱们现实的人使用,而不是创建虚拟空间,去模仿现实。

以Eric Evans(DDD之父)在他的书中的一个货物运输系统为例子简单说明一下。在通过一些用户需求讨论以后,在用户需求相对明朗以后,Eric这样描述领域模型:

  1. 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每一个Customer承担不一样的角色;
  2. Cargo的运送目标已指定,即Cargo有一个运送目标;
  3. 由一系列知足Specification(规格)的Carrier Movement(运输动做)来完成运输目标;

从上面的描述咱们能够看出,他彻底没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。上述这段描述彻底以货物为中心,把客户当作是货物在某个场景中可能会涉及到的关联角色,如货物会涉及到托运人、收货人、付款人;货物有一个肯定的目标,货物会通过一系列列的运输动做到达目的地;其实,我以为以用户为中心来思考领域模型的思惟只是停留在需求的表面,而没有挖掘出真正的需求的本质;咱们在作领域建模时须要努力挖掘用户需求的本质,这样才能真正实现用户需求;

关于用户、参与者这两个概念的区分,能够看一下下面的例子:

试想两我的共同玩足球游戏,操做者(用户)是驱动者,它驱使足球比赛领域中,各个“人”(参与者)的活动。这里立下一个假设,假设操做者A操做某一队员a,而队员a拥有着某人B的信息,那么有如下说法,a是B的镜像,a是领域参与者,A是驱动者。

设计领域模型的通常步骤

  1. 根据需求创建一个初步的领域模型,识别出一些明显的领域概念以及它们的关联,关联能够暂时没有方向但须要有(1:1,1:N,M:N)这些关系;能够用文字精确的没有歧义的描述出每一个领域概念的涵义以及包含的主要信息;
  2. 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;
  3. 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务;
  4. 分析关联,经过对业务的更深刻分析以及各类软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不须要的关联;
  5. 找出聚合边界及聚合根,这是一件颇有难度的事情;由于你在分析的过程当中每每会碰到不少模棱两可的难以清晰判断的选择问题,因此,须要咱们平时一些分析经验的积累才能找出正确的聚合根;
  6. 为聚合根配备仓储,通常状况下是为一个聚合分配一个仓储,此时只要设计好仓储的接口便可;
  7. 走查场景,肯定咱们设计的领域模型可以有效地解决业务需求;
  8. 考虑如何建立领域实体或值对象,是经过工厂仍是直接经过构造函数;
  9. 停下来重构模型。寻找模型中以为有些疑问或者是蹩脚的地方,好比思考一些对象应该经过关联导航获得仍是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等;

领域建模是一个不断重构,持续完善模型的过程,你们会在讨论中将变化的部分反映到模型中,从而是模型不断细化并朝正确的方向走。领域建模是领域专家、设计人员、开发人员之间沟通交流的过程,是你们工做和思考问题的基础。

领域驱动设计的经典分层架构

img

用户界面/展示层

负责向用户展示信息以及解释用户命令。更细的方面来说就是:

  1. 请求应用层以获取用户所须要展示的数据;
  2. 发送命令给应用层要求其执行某个用户命令;

应用层

很薄的一层,定义软件要完成的全部任务。对外为展示层提供各类应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各类业务逻辑,应用层不包含业务逻辑。

领域层

负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。

基础设施层

本层为其余层提供通用的技术能力;提供了层间的通讯;为领域层实现持久化机制;总之,基础设施层能够经过架构和框架来支持其余层的技术需求;

领域驱动设计过程当中使用的模式

全部模式的总揽图

img

关联的设计

关联自己不是一个模式,但它在领域建模的过程当中很是重要,因此须要在探讨各类模式以前,先讨论一下对象之间的关联该如何设计。我以为对象的关联的设计能够遵循以下的一些原则:

  1. 关联尽可能少,对象之间的复杂的关联容易造成对象的关系网,这样对于咱们理解和维护单个对象很不利,同时也很难划分对象与对象之间的边界;另外,同时减小关联有助于简化对象之间的遍历;
  2. 对多的关联也许在业务上是很天然的,一般咱们会用一个集合来表示1对多的关系。但咱们每每也须要考虑到性能问题,尤为是当集合内元素很是多的时候,此时每每须要经过单独查询来获取关联的集合信息;
  3. 关联尽可能保持单向的关联;
  4. 在创建关联时,咱们须要深刻去挖掘是否存在关联的限制条件,若是存在,那么最好把这个限制条件加到这个关联上;每每这样的限制条件能将关联化繁为简,便可以将多对多简化为1对多,或将1对多简化为1对1;

实体(Entity)

实体就是领域中须要惟一标识的领域概念。由于咱们有时须要区分是哪一个实体。有两个实体,若是惟一标识不同,那么即使实体的其余全部属性都同样,咱们也认为他们两个不一样的实体;由于实体有生命周期,实体从被建立后可能会被持久化到数据库,而后某个时候又会被取出来。因此,若是咱们不为实体定义一种能够惟一区分的标识,那咱们就没法区分究竟是这个实体仍是哪一个实体。另外,不该该给实体定义太多的属性或行为,而应该寻找关联,发现其余一些实体或值对象,将属性或行为转移到其余关联的实体或值对象上。好比Customer实体,他有一些地址信息,因为地址信息是一个完整的有业务含义的概念,因此,咱们能够定义一个Address对象,而后把Customer的地址相关的信息转移到Address对象上。若是没有Address对象,而把这些地址信息直接放在Customer对象上,而且若是对于一些其余的相似Address的信息也都直接放在Customer上,会致使Customer对象很混乱,结构不清晰,最终致使它难以维护和理解;

值对象(Value Object)

在领域中,并非没一个事物都必须有一个惟一标识,也就是说咱们不关心对象是哪一个,而只关心对象是什么。就以上面的地址对象Address为例,若是有两个Customer的地址信息是同样的,咱们就会认为这两个Customer的地址是同一个。也就是说只要地址信息同样,咱们就认为是同一个地址。用程序的方式来表达就是,若是两个对象的全部的属性的值都相同咱们会认为它们是同一个对象的话,那么咱们就能够把这种对象设计为值对象。所以,值对象没有惟一标识,这是它和实体的最大不一样。另外值对象在判断是不是同一个对象时是经过它们的全部属性是否相同,若是相同则认为是同一个值对象;而咱们在区分是不是同一个实体时,只看实体的惟一标识是否相同,而无论实体的属性是否相同;值对象另一个明显的特征是不可变,即全部属性都是只读的。由于属性是只读的,因此能够被安全的共享;当共享值对象时,通常有复制和共享两种作法,具体采用哪一种作法还要根据实际状况而定;另外,咱们应该给值对象设计的尽可能简单,不要让它引用不少其余的对象,由于他只是一个值,就像int a = 3;那么”3”就是一个咱们传统意义上所说的值,而值对象其实也能够和这里的”3”同样理解,也是一个值,只不过是用对象来表示。因此,当咱们在C#语言中比较两个值对象是否相等时,会重写GetHashCode和Equals这两个方法,目的就是为了比较对象的值;值对象虽然是只读的,可是能够被整个替换掉。就像你把a的值修改成”4”(a = 4;)同样,直接把”3”这个值替换为”4”了。值对象也是同样,当你要修改Customer的Address对象引用时,不是经过Customer.Address.Street这样的方式来实现,由于值对象是只读的,它是一个完整的不可分割的总体。咱们能够这样作:Customer.Address = new Address(…);

领域服务(Domain Service)

领域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,由于它们本质上就是一些操做,一些动做,而不是事物。这些操做或动做每每会涉及到多个领域对象,而且须要协调这些领域对象共同完成这个操做或动做。若是强行将这些操做职责分配给任何一个对象,则被分配的对象就是承担一些不应承担的职责,从而会致使对象的职责不明确很混乱。可是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。因此咱们须要寻找一种新的模式来表示这种跨多个对象的操做,DDD认为服务是一个很天然的范式用来对应这种跨多个对象的操做,因此就有了领域服务这个模式。和领域对象不一样,领域服务是以动词开头来命名的,好比资金转账服务能够命名为MoneyTransferService。固然,你也能够把服务理解为一个对象,但这和通常意义上的对象有些区别。由于通常的领域对象都是有状态和行为的,而领域服务没有状态只有行为。须要强调的是领域服务是无状态的,它存在的意义就是协调领域对象共完成某个操做,全部的状态仍是都保存在相应的领域对象中。我以为模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各类现象,有动有静,有独立有协做。

领域服务还有一个很重要的功能就是能够避免领域逻辑泄露到应用层。由于若是没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该作的操做,这样一来,领域层可能会把一部分领域知识泄露到应用层。由于应用层须要了解每一个领域对象的业务功能,具备哪些信息,以及它可能会与哪些其余领域对象交互,怎么交互等一系列领域知识。所以,引入领域服务能够有效的防治领域层的逻辑泄露到应用层。对于应用层来讲,从可理解的角度来说,经过调用领域服务提供的简单易懂但意义明确的接口确定也要比直接操纵领域对象容易的多。这里彷佛也看到了领域服务具备Façade的功能,呵呵。

说到领域服务,还须要提一下软件中通常有三种服务:应用层服务、领域服务、基础服务。

应用层服务

  1. 获取输入(如一个XML请求);
  2. 发送消息给领域层服务,要求其实现转账的业务逻辑;
  3. 领域层服务处理成功,则调用基础层服务发送Email通知;

领域层服务

  1. 获取源账号和目标账号,分别通知源账号和目标账号进行扣除金额和增长金额的操做;
  2. 提供返回结果给应用层;

基础层服务

按照应用层的请求,发送Email通知;

因此,从上面的例子中能够清晰的看出,每种服务的职责;

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

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

聚合有如下一些特色:

  1. 每一个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体;
  2. 聚合内部的对象之间能够相互引用,可是聚合外部若是要访问聚合内部的对象时,必须经过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部能够保持 对它的引用的惟一元素;
  3. 聚合内除根之外的其余实体的惟一标识都是本地标识,也就是只要在聚合内部保持惟一便可,由于它们老是从属于这个聚合的;
  4. 聚合根负责与外部其余对象打交道并维护本身内部的业务规则;
  5. 基于聚合的以上概念,咱们能够推论出从数据库查询时的单元也是以聚合为一个单元,也就是说咱们不能直接查询聚合内部的某个非根的对象;
  6. 聚合内部的对象能够保持对其余聚合根的引用;
  7. 删除一个聚合根时必须同时删除该聚合内的全部相关对象,由于他们都同属于一个聚合,是一个完整的概念;

关于如何识别聚合以及聚合根的问题:

我以为咱们能够先从业务的角度深刻思考,而后慢慢分析出有哪些对象是:

  1. 有独立存在的意义,即它是不依赖于其余对象的存在它才有意义的;
  2. 能够被独立访问的,仍是必须经过某个其余对象导航获得的;

如何识别聚合?

我以为这个须要从业务的角度深刻分析哪些对象它们的关系是内聚的,即咱们会把他们当作是一个总体来考虑的;而后这些对象咱们就能够把它们放在一个聚合内。所谓关系是内聚的,是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当咱们在修改一个聚合时,咱们必须在事务级别确保整个聚合内的全部对象知足这个固定规则。做为一条建议,聚合尽可能不要太大,不然即使可以作到在事务级别保持聚合的业务规则完整性,也可能会带来必定的性能问题。有分析报告显示,一般在大部分领域模型中,有70%的聚合一般只有一个实体,即聚合根,该实体内部没有包含其余实体,只包含一些值对象;另外30%的聚合中,基本上也只包含两到三个实体。这意味着大部分的聚合都只是一个实体,该实体同时也是聚合根。

如何识别聚合根?

若是一个聚合只有一个实体,那么这个实体就是聚合根;若是有多个实体,那么咱们能够思考聚合内哪一个对象有独立存在的意义而且能够和外部直接进行交互。

工厂(Factory)

DDD中的工厂也是一种体现封装思想的模式。DDD中引入工厂模式的缘由是:有时建立一个领域对象是一件比较复杂的事情,不只仅是简单的new操做。正如对象封装了内部实现同样(咱们无需知道对象的内部实现就可使用对象的行为),工厂则是用来封装建立一个复杂对象尤为是聚合时所需的知识,工厂的做用是将建立对象的细节隐藏起来。客户传递给工厂一些简单的参数,而后工厂能够在内部建立出一个复杂的领域对象而后返回给客户。领域模型中其余元素都不适合作这个事情,因此须要引入这个新的模式,工厂。工厂在建立一个复杂的领域对象时,一般会知道该知足什么业务规则(它知道先怎样实例化一个对象,而后在对这个对象作哪些初始化操做,这些知识就是建立对象的细节),若是传递进来的参数符合建立对象的业务规则,则能够顺利建立相应的对象;可是若是因为参数无效等缘由不能建立出指望的对象时,应该抛出一个异常,以确保不会建立出一个错误的对象。固然咱们也并不老是须要经过工厂来建立对象,事实上大部分状况下领域对象的建立都不会太复杂,因此咱们只须要简单的使用构造函数建立对象就能够了。隐藏建立对象的好处是显而易见的,这样能够不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只须要简单的调用领域工厂建立出指望的对象便可。

仓储(Repository)

  1. 仓储被设计出来的目的是基于这个缘由:领域模型中的对象自从被建立出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,而后当须要的时候咱们会重建该对象;重建对象就是根据数据库中已存储的对象的状态从新建立对象的过程;因此,可见重建对象是一个和数据库打交道的过程。从更广义的角度来理解,咱们常常会像集合同样从某个相似集合的地方根据某个条件获取一个或一些对象,往集合中添加对象或移除对象。也就是说,咱们须要提供一种机制,能够提供相似集合的接口来帮助咱们管理对象。仓储就是基于这样的思想被设计出来的;
  2. 仓储里面存放的对象必定是聚合,缘由是以前提到的领域模型中是以聚合的概念去划分边界的;聚合是咱们更新对象的一个边界,事实上咱们把整个聚合当作是一个总体概念,要么一块儿被取出来,要么一块儿被删除。咱们永远不会单独对某个聚合内的子对象进行单独查询或作更新操做。所以,咱们只对聚合设计仓储。
  3. 仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中咱们定义仓储的接口,而在基础设施层实现具体的仓储。这样作的缘由是:因为仓储背后的实现都是在和数据库打交道,可是咱们又不但愿客户(如应用层)把重点放在如何从数据库获取数据的问题上,由于这样作会致使客户(应用层)代码很混乱,极可能会所以而忽略了领域模型的存在。因此咱们须要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可让它专心的不会被什么数据访问代码打扰的状况下协调领域对象完成业务逻辑。这种经过接口来隔离封装变化的作法其实很常见。因为客户面对的是抽象的接口并非具体的实现,因此咱们能够随时替换仓储的真实实现,这颇有助于咱们作单元测试。
  4. 尽管仓储能够像集合同样在内存中管理对象,可是仓储通常不负责事务处理。通常事务处理会交给一个叫“工做单元(Unit Of Work)”的东西。关于工做单元的详细信息我在下面的讨论中会讲到。
  5. 另外,仓储在设计查询接口时,可能还会用到规格模式(Specification Pattern),我见过的最厉害的规格模式应该就是LINQ以及DLINQ查询了。通常咱们会根据项目中查询的灵活度要求来选择适合的仓储查询接口设计。一般状况下只须要定义简单明了的具备固定查询参数的查询接口就能够了。只有是在查询条件是动态指定的状况下才可能须要用到Specification等模式。

在分层架构中其余层如何与领域层交互

从经典的领域驱动设计分层架构中能够看出,领域层的上层是应用层,下层是基础设施层。那么领域层是如何与其它层交互的呢?

对于会影响领域层中领域对象状态的应用层功能

通常应用层会先启动一个工做单元,而后:

  1. 对于修改领域对象的状况,经过仓储获取领域对象,调用领域对象的相关业务方法以完成业务逻辑处理;
  2. 对于新增领域对象的状况,经过构造函数或工厂建立出领域对象,若是须要还能够继续对该新建立的领域对象作一些操做,而后把该新建立的领域对象添加到仓储中;
  3. 对于删除领域对象的状况,能够先把领域对象从仓储中取出来,而后将其从仓储中删除,也能够直接传递一个要删除的领域对象的惟一标识给仓储通知其移除该惟一标识对应领域对象;
  4. 若是一个业务逻辑涉及到多个领域对象,则调用领域层中的相关领域服务完成操做;

注意,以上所说的全部领域对象都是只聚合根,另外在应用层须要获取仓储接口以及领域服务接口时,均可以经过IOC容器获取。最后通知工做单元提交事务从而将全部相关的领域对象的状态以事务的方式持久化到数据库;

关于Unit of Work(工做单元)的几种实现方法

  1. 基于快照的实现,即领域对象被取出来后,会先保存一个备份的对象,而后当在作持久化操做时,将最新的对象的状态和备份的对象的状态进行比较,若是不相同,则认为有作过修改,而后进行持久化;这种设计的好处是对象不用告诉工做单元本身的状态修改了,而缺点也是显而易见的,那就是性能可能会低,备份对象以及比较对象的状态是否有修改的过程在当对象自己很复杂的时候,每每是一个比较耗时的步骤,并且要真正实现对象的深拷贝以及判断属性是否修改仍是比较困难的;
  2. 不基于快照,而是仓储的相关更新或新增或删除接口被调用时,仓储通知工做单元某个对象被新增了或更新了或删除了。这样工做单元在作数据持久化时也一样能够知道须要持久化哪些对象了;这种方法理论上不须要ORM框架的支持,对领域模型也没有任何倾入性,同时也很好的支持了工做单元的模式。对于不想用高级ORM框架的朋友来讲,这种方法挺好;
  3. 不基于快照,也不用仓储告诉工做单元数据更改了。而是采用AOP的思想,采用透明代理的方式进行一个拦截。在NHibernate中,咱们的属性一般要被声明为virtual的,一个缘由就是NHibernate会生成一个透明代理,用于拦截对象的属性被修改时,自动通知工做单元对象的状态被更新了。这样工做单元也一样知道须要持久化哪些对象了。这种方法对领域模型的倾入性不大,而且能很好的支持工做单元模式,若是用NHibernate做为ORM,这种方法用的比较多;
  4. 通常是微软用的方法,那就是让领域对象实现.NET框架中的INotifiyPropertyChanged接口,而后在每一个属性的set方法的最后一行调用OnPropertyChanged的方法从而显示地通知别人本身的状态修改了。这种方法相对来讲对领域模型的倾入性最强。

对于不会影响领域层中领域对象状态的查询功能

能够直接经过仓储查询出所须要的数据。但通常领域层中的仓储提供的查询功能也许不能知足界面显示的须要,则可能须要屡次调用不一样的仓储才能获取所须要显示的数据;其实针对这种查询的状况,我在后面会讲到能够直接经过CQRS的架构来实现。即对于查询,咱们能够在应用层不调用领域层的任何东西,而是直接经过某个其余的用另外的技术架构实现的查询引擎来完成查询,好比直接经过构造参数化SQL的方式从数据库一个表或多个表中查询出任何想要显示的数据。这样不只性能高,也能够减轻领域层的负担。领域模型不太适合为应用层提供各类查询服务,由于每每界面上要显示的数据是不少对象的组合信息,是一种非对象概念的信息,就像报表;

领域驱动设计的其余一些主题

上面只是涉及到DDD中最基本的内容,DDD中还有不少其余重要的内容在上面没有提到,如:

  1. 模型上下文、上下文映射、上下文共享;
  2. 如何将分析模式和设计模式运用到DDD中;
  3. 一些关于柔性设计的技巧;
  4. 若是保持模型完整性,以及持续集成方面的知识;
  5. 如何精炼模型,识别核心模型以及通用子领域;

这些主题都很重要,由于篇幅有限以及我目前掌握的知识也有限,而且为了突出这篇文章的重点,因此不对他们作详细介绍了,你们有兴趣的能够本身阅读一下。

一些相关的扩展阅读

CQRS架构

核心思想是将应用程序的查询部分和命令部分彻底分离,这两部分能够用彻底不一样的模型和技术去实现。好比命令部分能够经过领域驱动设计来实现;查询部分能够直接用最快的非面向对象的方式去实现,好比用SQL。这样的思想有不少好处:

  1. 实现命令部分的领域模型不用常常为了领域对象可能会被如何查询而作一些折中处理;
  2. 因为命令和查询是彻底分离的,因此这两部分能够用不一样的技术架构实现,包括数据库设计均可以分开设计,每一部分能够充分发挥其长处;
  3. 高性能,命令端由于没有返回值,能够像消息队列同样接受命令,放在队列中,慢慢处理;处理完后,能够经过异步的方式通知查询端,这样查询端能够作数据同步的处理;

Event Sourcing(事件溯源)

对于DDD中的聚合,不保存聚合的当前状态,而是保存对象上所发生的每一个事件。当要重建一个聚合对象时,能够经过回溯这些事件(即让这些事件从新发生)来让对象恢复到某个特定的状态;由于有时一个聚合可能会发生不少事件,因此若是每次要在重建对象时都从头回溯事件,会致使性能低下,因此咱们会在必定时候为聚合建立一个快照。这样,咱们就能够基于某个快照开始建立聚合对象了。

DCI架构

DCI架构强调,软件应该真实的模拟现实生活中对象的交互方式,代码应该准确朴实的反映用户的心智模型。在DCI中有:数据模型、角色模型、以及上下文这三个概念。数据模型表示程序的结构,目前咱们所理解的DDD中的领域模型能够很好的表示数据模型;角色模型表示数据如何交互,一个角色定义了某个“身份”所具备的交互行为;上下文对应业务场景,用于实现业务用例,注意是业务用例而不是系统用例,业务用例只与业务相关;软件运行时,根据用户的操做,系统建立相应的场景,并把相关的数据对象做为场景参与者传递给场景,而后场景知道该为每一个对象赋予什么角色,当对象被赋予某个角色后就真正成为有交互能力的对象,而后与其余对象进行交互;这个过程与现实生活中咱们所理解的对象是一致的;

DCI的这种思想与DDD中的领域服务所作的事情是同样的,但实现的角度有些不一样。DDD中的领域服务被建立的出发点是当一些职责不太适合放在任何一个领域对象上时,这个职责每每对应领域中的某个活动或转换过程,此时咱们应该考虑将其放在一个服务中。好比资金转账的例子,咱们应该提供一个资金转账的服务,用来对应领域中的资金转账这个领域概念。可是领域服务内部作的事情是协调多个领域对象完成一件事情。所以,在DDD中的领域服务在协调领域对象作事情时,领域对象每每是处于一个被动的地位,领域服务通知每一个对象要求其作本身能作的事情,这样就好了。这个过程当中咱们彷佛看不到对象之间交互的意思,由于整个过程都是由领域服务以面向过程的思惟去实现了。而DCI则通用引入角色,赋予角色以交互能力,而后让角色之间进行交互,从而可让咱们看到对象与对象之间交互的过程。但前提是,对象之间确实是在交互。由于现实生活中并非全部的对象在作交互,好比有A、B、C三个对象,A通知B作事情,A通知C作事情,此时能够认为A和B,A和C之间是在交互,可是B和C之间没有交互。因此咱们须要分清这种状况。资金转账的例子,A至关于转账服务,B至关于账号1,C至关于账号2。所以,资金转账这个业务场景,用领域服务比较天然。有人认为DCI能够替换DDD中的领域服务,我持怀疑态度。

四色原型分析模式

时刻-时间段原型(Moment-Interval Archetype)

表示在某个时刻或某一段时间内发生的某个活动。使用粉红色表示,简写为MI。

参与方-地点-物品原型(Part-Place-Thing Archetype)

表示参与某个活动的人或物,地点则是活动的发生地。使用绿色表示。简写为PPT。

描述原型(Description Archetype)

表示对PPT的本质描述。它不是PPT的分类!Description是从PPT抽象出来的不变的共性的属性的集合。使用蓝色表示,简写为DESC。

举个例子,有一我的叫张三,若是某个外星人问你张三是什么?你会怎么说?可能会说,张三是我的,可是外星人不知道“人”是什么。而后你会怎么办?你就会说:张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经能够借用这个例子向你们说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但咱们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。因此就有所谓的张三是人的说法。

角色原型(Role Archetype)

角色就是咱们平时所理解的“身份”。使用黄色表示,简写为Role。为何会有角色这个概念?由于有些活动,只容许具备特定角色(身份)的PPT(参与者)才能参与该活动。好比一我的只有具备教师的角色才能上课(一种活动);一我的只有是一个合法公民才能参与选举和被选举;可是有些活动也是不须要角色的,好比一我的不须要具有任何角色就能够睡觉(一种活动)。固然,其实说人不须要角色就能睡觉也是错误的,错在哪里?由于咱们能够这样理解:一个客观存在只要具备“人”的角色就能睡觉,其实这时候,咱们已经把DESC看成角色来看待了。因此,其实角色这个概念是很是广的,不能用咱们平时所理解的狭义的“身份”来理解,由于“教师”、“合法公民”、“人”均可以被做为角色来看待。所以,应该这样说:任何一个活动,都须要具备必定角色的参与者才能参与。

用一句话来归纳四色原型就是:一个什么什么样的人或组织或物品以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动"就是MI。

以上这些东西若是在学习了DDD以后再去学习会对DDD有更深刻的了解,但我以为DDD相对比较基础,若是咱们在已经了解了DDD的基础之上再去学习这些东西会更加有效和容易掌握。

但愿本文对你们有所帮助。

相关文章
相关标签/搜索