DDD(DomainDriverDesigner)领域驱动设计简介

更新时间:2023-05-11 03:52:34 阅读: 评论:0

DDD(DomainDriverDesigner)领域驱动设计简介
领域驱动设计之领域模型
加⼀个导航,关于如何设计聚合的详细思考,见⽂章。
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. 请求应⽤层以获取⽤户所需要展现的数据;
2. 发送命令给应⽤层要求其执⾏某个⽤户命令;
应⽤层
很薄的⼀层,定义软件要完成的所有任务。对外为展现层提供各种应⽤功能(包括查询或命令),对内调⽤领域层(领域对象或领域服务)完成各种业务逻辑,应⽤层不包含业务逻辑。
领域层
负责表达业务概念,业务状态信息以及业务规则,领域模型处于这⼀层,是业务软件的核⼼。
基础设施层
本层为其他层提供通⽤的技术能⼒;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来⽀持其他层的技术需求;
领域驱动设计过程中使⽤的模式
所有模式的总揽图
关联的设计
关联本⾝不是⼀个模式,但它在领域建模的过程中⾮常重要,所以需要在探讨各种模式之前,先讨论⼀下对象之间的关联该如何设计。我觉得对象的关联的设计可以遵循如下的⼀些原则:
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. 根据需求建⽴⼀个初步的领域模型,识别出⼀些明显的领域概念以及它们的关联,关联可以暂时没
有⽅向但需要有(1:1,1:
N,M:N)这些关系;可以⽤⽂字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息;
2. 分析主要的软件应⽤程序功能,识别出主要的应⽤层的类;这样有助于及早发现哪些是应⽤层的职责,哪些是领域层的职责;
3. 进⼀步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务;
4. 分析关联,通过对业务的更深⼊分析以及各种软件设计原则及性能⽅⾯的权衡,明确关联的⽅向或者去掉⼀些不需要的关联;
5. 找出聚合边界及聚合根,这是⼀件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所
以,需要我们平时⼀些分析经验的积累才能找出正确的聚合根;
6. 为聚合根配备仓储,⼀般情况下是为⼀个聚合分配⼀个仓储,此时只要设计好仓储的接⼝即可;
7. ⾛查场景,确定我们设计的领域模型能够有效地解决业务需求;
8. 考虑如何创建领域实体或值对象,是通过⼯⼚还是直接通过构造函数;
9. 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地⽅,⽐如思考⼀些对象应该通过关联导航得到还是应该从仓储获取?聚合
设计的是否正确?考虑模型的性能怎样,等等;
领域建模是⼀个不断重构,持续完善模型的过程,⼤家会在讨论中将变化的部分反映到模型中,从⽽是模型不断细化并朝正确的⽅向⾛。领域建模是领域专家、设计⼈员、开发⼈员之间沟通交流的过程,是⼤家⼯作和思考问题的基础。
在分层架构中其他层如何与领域层交互
从经典的领域驱动设计分层架构中可以看出,领域层的上层是应⽤层,下层是基础设施层。那么领域层是如何与其它层交互的呢?
对于会影响领域层中领域对象状态的应⽤层功能
⼀般应⽤层会先启动⼀个⼯作单元,然后:
1. 对于修改领域对象的情况,通过仓储获取领域对象,调⽤领域对象的相关业务⽅法以完成业务逻辑处理;
2. 对于新增领域对象的情况,通过构造函数或⼯⼚创建出领域对象,如果需要还可以继续对该新创建的领域对象做⼀些操作,然后把该
新创建的领域对象添加到仓储中;
3. 对于删除领域对象的情况,可以先把领域对象从仓储中取出来,然后将其从仓储中删除,也可以直接传递⼀个要删除的领域对象的唯
⼀标识给仓储通知其移除该唯⼀标识对应领域对象;
4. 如果⼀个业务逻辑涉及到多个领域对象,则调⽤领域层中的相关领域服务完成操作;
注意,以上所说的所有领域对象都是只聚合根,另外在应⽤层需要获取仓储接⼝以及领域服务接⼝时,都可以通过IOC容器获取。最后通知⼯作单元提交事务从⽽将所有相关的领域对象的状态以事务的⽅式持久化到数据库;
关于Unit of Work(⼯作单元)的⼏种实现⽅法
1. 基于快照的实现,即领域对象被取出来后,会先保存⼀个备份的对象,然后当在做持久化操作时,将最新的对象的状态和备份的对象
的状态进⾏⽐较,如果不相同,则认为有做过修改,然后进⾏持久化;这种设计的好处是对象不⽤告诉⼯作单元⾃⼰的状态修改了,⽽缺点也是显⽽易见的,那就是性能可能会低,备份对象以及⽐较对象的状态是否有修改的过程在当对象本⾝很复杂的时候,往往是⼀个⽐较耗时的步骤,⽽且要真正实现对象的深拷贝以及判断属性是否修改还是⽐较困难的;
2. 不基于快照,⽽是仓储的相关更新或新增或删除接⼝被调⽤时,仓储通知⼯作单元某个对象被新增了或更新了或删除了。这样⼯作单
元在做数据持久化时也同样可以知道需要持久化哪些对象了;这种⽅法理论上不需要ORM框架的⽀持,对领域模型也没有任何倾⼊性,同时也很好的⽀持了⼯作单元的模式。对于不想⽤⾼级ORM框架的朋友来说,这种⽅法挺好;
3. 不基于快照,也不⽤仓储告诉⼯作单元数据更改了。⽽是采⽤AOP的思想,采⽤透明代理的⽅式进⾏⼀个拦截。在NHibernate中,我
们的属性通常要被声明为virtual的,⼀个原因就是NHibernate会⽣成⼀个透明代理,⽤于拦截对象的
属性被修改时,⾃动通知⼯作单元对象的状态被更新了。这样⼯作单元也同样知道需要持久化哪些对象了。这种⽅法对领域模型的倾⼊性不⼤,并且能很好的⽀持⼯

本文发布于:2023-05-11 03:52:34,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/881623.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:领域   对象   模型   聚合   设计   业务
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图