实现领域模型Word下载.docx
《实现领域模型Word下载.docx》由会员分享,可在线阅读,更多相关《实现领域模型Word下载.docx(42页珍藏版)》请在冰豆网上搜索。
2领域、子域和限界上下文
从广义上来讲,领域就是一个组织所做的事情以及其中所包含的一切。
在一个好的限界上下文中,每一个术语应该仅表示一种领域概念。
一个子域对应一个限界上下文这是基本标准,我们尽量不要让子域和限界上下文相交
我们工作的关注点应该放在核心域上。
2.1战略设计
在设计开发时不应该一上来就去关注实体、值对象,而应该用更广阔的视野,先去考虑战略设计(划分子域、限界上下文)再去考虑战术设计。
在划分子域与限界上下文时一定不能都放在一个域或者一个上下文中,因为那样会导致一个大而全的大泥球架构,会导致系统以后很难进行维护、分布式部署。
对于领域内的内容不要按照表结构去分析,而要利用四色原型去分析。
2.2理解限界上下文
限界上下文就是一个显示的边界(语义上的边界,我们目前用框图来表示限界上下文,但框图并不是限界上下文本身),领域模型则位于边界之内。
在边界内,通用语言中的所有术语和词组都有特定的含义(如果再不同的上下文内,看到了相同的对象,那么通常认为此模型是错误的,除非这些上下文使用了共享内核),而模型需要准确地反映通用语言。
一个限界上下文可以包括领域模型(主要公民)、模块、聚合、领域服务、领域事件、应用服务、面向服务的组件(Webservice/Rest/Jms)、数据库的Schema等。
我们应该拒绝使用智能UI(界面显示、事务、逻辑混合在一起)。
也不需要利用用户界面中对领域进行建模,这样会导致贫血对象。
设计限界上下文应该注意的东西:
●不要采用架构来指导设计开发上下文,应该用通用语言
●不要根据任务的分配来拆分上下文,尽量保证一个团队一个上下文,尽量少使用共享内核
●使用模块可以避免创建一些微小的上下文。
3上下文映射图
一个项目的上下文映射图可以画一个简单的框图来表示两个或多个限界上下文之间的映射关系。
尽早的绘制上下文映射图,可以迫使你仔细思考你的项目和你所依赖项目之间的关系。
上下文映射图并不是一种企业架构,也不是系统拓扑图,他是一种更高层次的架构,上下文映射图展现了一种上下文之间的组织动态能力。
常用的集成模式:
●开放主机服务:
该模式一般通过REST实现,我们一般将开放主机服务看成是远程方法调用的API。
●发布语言:
在使用REST服务时,发布语言用来表示领域概念,此时可以使用XML和JSON
●防腐层:
在使用REST时,客户端的领域服务奖访问远程的开放主机服务,远程服务器以发布语言的形式返回,下游的防腐层将返回内容翻译成本地上下文中的领域对象。
4架构
4.1分层架构
分层架构分为两种:
在严格分层架构中,某层只能与直接位于其下方的层发生耦合,而松散分层架构则允许任意上方层与任意下方层发生耦合。
由于用户界面层和应用服务通常需要与基础设施层打交道,所以大多数的分层架构属于松散分层架构。
传统-松散分层架构
用户界面只用于处理用户显示和用户请求,而不应该包含领域或者业务逻辑,有人说既然用户界面要对用户的输入进行验证,那么就应该包含业务逻辑。
其实,用户界面进行的验证和对领域模型进行的验证是不同的,所以用户界面不应该包含业务逻辑。
但是如果用户界面使用了领域模型中的对象,此时的领域对象也仅应该用于数据的渲染展现。
可以利用展现模型对用户界面和领域对象进行解耦。
用户可能是人,也有可能是其他系统,所以有时候用户界面将采用开发主机服务的方式对外提供API
用户界面是应用层的客户。
应用服务在应用层中,应用服务和领域服务也是不同的,所以业务逻辑也不应该出现在应用服务中,应用服务是领域模型的直接直接客户。
应用服务的通常用途是:
接收来自用户界面的输入参数,再通过资源库获取到聚合实例,然后再执行相应的命令操作。
或者应用服务使用工厂和聚合的构造函数来实例化对象,然后采用资源库对其进行持久化。
当领域模型用于发布领域事件时,应用层可以将订阅方注册到事件上。
在传统的分层架构中,领域模型中有些接口的实现会依赖于基础设施层,比如资源库接口的实现需要基础设施层提供的持久化机制,如果我们将资源库的接口实现放在基础设施层中,那么从基础设施层向上引用领域层的东西就会违背了分层架构的原则。
此时我们可以采用模块化的方式来隐藏技术实现细节或者将领域层的接口实现放在应用层中,这样就把基础设施层中的所有组件和框架看做是应用程序的低层服务。
如图:
分层架构-领域层接口实现
4.1.1依赖倒置原则
高层模块不应该依赖低层模块,两者都该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象。
根据该定义,低层服务(基础设施层)应该依赖于高层服务(用户接口层、应用层、领域层)所提供的接口
分层架构-依赖倒置原则
基础设施层、领域层都会依赖领域模型定义的接口,应用层是领域层的直接客户,它可以间接的访问资源库和基础设施层提供的实现。
比如通过依赖注入、服务工程、插件的方式获取这些资源。
4.2六边形架构
目前很多的分层架构实际上就是六边形架构,这是因为很多项目使用了依赖注入,依赖注入本身就具有了适配器与端口的风格。
我们应该根据功能需求与用例来设计适配器而不应该按照客户数量和输出机制来设计。
就像在分层架构中一样,应用服务还是领域模型的直接客户,应用层的应用服务对外提供API。
六边形架构的优点有两个,一是测试方便,二是安全(提供了良好的封装,应用服务和领域模型不会泄露到外部区域)。
六边形架构为面向服务的架构、REST、事件驱动架构、CQRS、数据网织、基于网格的分布式缓存、Map-Reduce分布式处理等提供了坚实的基础。
4.3面向服务架构
使用SOA时要注意以下两点:
●业务价值高于技术策略/服务
●战略目标高于项目目标
在限界上下文中我们注意到不应该使架构对模型的大小产生影响,所以我们不应该以技术服务端点(SOAP接口、REST资源、消息类型)来设计限界上下文。
4.4REST
Http存在的优点:
●无状态通信,采用具有自描述功能的消息,比如HTTP请求便包含了服务器所需的所有信息
●HTTP方法(Get、Put、Delete)是幂等的,即我们可以安全的对失败的请求进行重试。
4.4.1REST和DDD
第一种方法就是为系统接口层单独创建一个限界上下文,然后再上下文中通过适当的策略访问领域模型,这是通过资源抽象将系统功能暴露给外界,而不是通过服务或者远程接口。
这种方式在领域模型和系统接口模型之间完成了解耦,我们对领域模型所做的修改可以不反应到系统对外的接口模型中。
系统对外接口模型的设计应该按照用户需求或者用例来做,而不应该按照领域模型来做。
第二种方法就是用于需要使用标准媒体类型的时候,如果某种媒体类型并不适用于支持单个系统接口,而是用于一组相似的客户端-服务器交互场景,此时我们可以创建一个领域模型来处理每一种媒体类型,这样的领域模型甚至可以在服务器和客户端之间重用。
这种方式的本质就是DDD中的共享内核或者发布语言
4.5命令和查询职责分离-CQRS
4.5.1产生原因
在页面显示时,我们有可能会显示来自不同聚合类型与实例的数据,领域越复杂,困难也越大。
这时一个资源库是解决不了这个问题的,我们可能需要从不同的资源库获取聚合实例,然后将这些实例组合成一个DTO交给页面显示。
或者在查询时利用特殊的查询方法将不同资源库中的聚合实例组合在一起(比如左右连接,union等)。
那么有没有一个方法可以将领域数据很方便的映射到页面中呢,那就得用到CQRS(Command-QueryResponsibilitySegregation)。
CQRS是一个架构模式。
4.5.2简要介绍
一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者都是。
比如下面这个方法
privateIntegerinteger;
publicIntegeradd(Integerfat){
integer=fat+1;
returninteger;
}
publicvoidcommand(Integerfat){
publicIntegerquery(){
}
也就是说:
●如果一个方法修改了对象的状态,那么这个对象就不应该在返回数据,它的返回值应该是void,这个方法称之为命令
●如果一个返回了数据,该方法就是一个查询方法,在此方法中就不应该有修改对象状态的动作。
在CQRS架构中将领域模型一分为二,变为命令模型和查询模型,也叫写模型和读模型。
其中命令模型中聚合应该只有add、save方法及一个利用聚合身份标识作为参数的查询方法如fromID()。
其余的查询方法都将放置在查询模型中。
4.5.3CQRS各个方面
4.5.3.1客户端和查询处理器
不管什么情况,查询处理器都表示一个只知道如何向数据库执行基本查询的简单组件。
建议返回给客户端的数据是序列化的XML和JSON
4.5.3.2查询模型
查询模型并不是一种规范化的数据模型,它不反映领域行为,只是用于页面数据显示的一种模型。
我们可以创建足够多的视图,用一个视图去对应一种界面显示或者用一张表对应一种界面显示。
当然有时候为了不创建太多的视图和表我们还可以在查询时利用过滤器和联合查询来实现界面显示。
CQRS的数据视图还可以采用事件源的方式实现,因为所有的事件都被持久化,这样我们就可以利用持久化的事件与快照从头重建显示视图,事件源使我们可以很方便的创建和维护视图以响应UI变化,此时的数据表也非常简单,就是单纯的领域事件数据。
4.5.3.3命令处理器
●分类风格:
多个处理器位于同一个应用服务中,根据命令类别来实现应用服务,该风格的最大优点是简单,维护方便,易于理解。
●专属风格:
每种命令都对应某个单独的类,并且该类自会有一个方法。
该风格的特点是职责单一,命令器之间相互独立。
●消息风格:
消息风格是专属风格的扩展,它利用消息服务器异步的处理命令,当处理能力不够时,可以利用增加服务器的方式来缓解负载。
不管采用什么风格的处理器,我们都应该使处理器之间解耦,绝不能使处理器依赖另一个处理器。
这样在更换处理器时才能使其不影响到别的处理器。
命令处理器在对聚合实例做完更新后(新建一个聚合实例,然后将该实例添加到资源库中或者从资源库中获取聚合实例并调用该实例的欣慰方法),同时命令模型还会发布一个领域事件,此事件将用于更新查询模型。
4.5.3.4命令模型
命令模型上的每个方法在执行完成时都将发布领域事件,如:
publicvoidcommitTo(SprintaSprint){
...
DomainEventPublisher
.instance()
.publish(newBackLogItemCommitted());
事件源并不一定与CQRS一起使用,除非事件日志包含在业务需求之中,不然命令模型是可以通过ORM等方式进行持久化的。
不管怎么样,我们都需要发布领域事件以更新查询模型。
4.5.3.5事件订阅器
事件订阅器根据命令模型发出的领域事件更新查询模型。
事件订阅器更新查询模型有两种方式,分别是同步和异步。
采用同步方式的话需要查询模型和命令模型共享同一个数据库,可以保证数据的完全一致性,但会造成性能下降、维护困难。
采用异步方式的话会使用户页面可能无法及时反映对命令模型的修改,对最终一致性带来挑战。
如果命令模型采用ORM作为持久化机制,那么我们可以用命令模型的数据存储来更新查询模型。
4.5.3.6最终一致性
采用异步更新命令模型和查询模型之间数据,查询模型将实现最终一致性。
一种方法就是在用户界面临时性的显示用户曾经提交到命令模型的数据。
但在一个用户进行操作,另一个用户却试图查看数据的时候会看到陈旧的数据。
如果这样的话,我们可以显示出查询模型中的时间以提醒用户进行数据更新。
当然这样我个人认为对用户是不友好的,所以我们在命令模型和查询模型之间以异步方式进行同步数据的时候会告知客户“您的请求已经正在处理,请稍候”。
当然到底是采用同步还是异步的方式进行查询模型的数据更新要根据客户的需求而定。
4.6事件驱动架构
事件驱动架构(Event-DrivenArchitecture,EDA)是一种用于处理事件生成、发现和处理等任务的软件架构。
有三种事件驱动的处理模式。
4.6.1管道和过滤器
由管道、过滤器、端口组成,用端口去连接管道和过滤器,所以六边形架构适合EDA,当然分层架构也适合EDA。
每个过滤器都是一个分离的组件,可以独立的处理任务。
为了提高效率一个过滤器可以同时处理多个管道的任务,一个管道的任务也可以让多个过滤器来处理。
4.6.2长时处理过程
将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤和任务完成情况进行持久化。
执行器和跟踪器可以合并为一个对象—聚合,这是最简单的方法。
通过该聚合来跟踪长时处理过程的状态(该对象在处理过程开始时创建,它将与所有领域事件共享一个唯一标识),而不需要开发一个单独的跟踪器来作为状态机。
这是实现基本长时处理过程最好的方法。
在六边形架构中,消息处理组件简单的将任务分发给应用服务或者命令处理器,自后应用服务加载目标聚合,在调用聚合上的命令方法,同样,聚合也会发出领域事件,此时表明聚合已经完成了它的处理任务。
在长时处理过程中我们要考虑时间的敏感性,我们既可以采用被动的超时检查(由执行器在每次并行执行流的完成事件到达时检查),也可以采用主动超时检查(通过一个外部定时器检查)
长时处理过程一般和分布式并行处理联系在一起,但它与分布式事务没有什么关系,长时处理过程需要的是最终一致性。
4.6.3事件源
对于聚合上的每次命令操作,都有至少一个领域事件发布出去,该领域事件描述了操作的执行结果,每一个领域事件都将保存到事件存储中。
我们还可以通过事件存储中的事件或者快照还原整个聚合在某个时刻的状态。
事件通常以二进制的形式保存在事件存储中,这样就说明事件源不适合查询操作
优点:
●向事件存储打补丁,可以消除系统bug
●可以利用重放事件的方式来重做和撤销对模型的修改
●重发事件,可以使业务层获得某些问题更好的解决答案
5实体
实体是领域模型中唯一用标识表示的对象,他不是用属性定义的对象。
两个Entity即使具有相同的属性,但标识不同,那么这两个Entity就不是一个Entity。
Entity的标识就是他的唯一标识,比如中国人的身份证号、信用卡号等。
在整个软件的生命周期内,实体具有连续性。
比如一个人5岁时拥有的属性与状态跟20岁时拥有的属性与状态肯定是不一样的,但我们不能说这不是同一个人。
在Entity中我们只添加那些重要的行为和属性,不要试图把Entity所拥有的所有属性都放到一个Entity中,这样会造成Entity的臃肿。
我们应该将一些不太重要的属性和行为转移到实体关联的其他对象中去,这些对象既可以是Entity对象,也可以使ValueObject对象。
例如Customer是有地址的,而地址是由几个属性(street、housenumber等)组成的,定义好Address对象后就可以将Customer对象中跟地址相关的属性转移到Address对象中去。
5.1唯一标识
唯一的身份标识和可变性特征将实体对象和值对象区分开来。
在设计实体时,我们首先考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始就关注实体的属性和行为。
可以利用值对象存放唯一标识。
5.1.1用户提供唯一标识
如果让用户在页面中输入唯一标识,那么我们将很难保证标识的唯一性。
所以我们一般将用户的输入作为实体的属性,这些属性可以用于对象匹配。
5.1.2应用程序生成唯一标识
利用UUID可以保证标识的唯一性,Java提供了这样的类:
java.util.UUID。
利用值对象来保存唯一标识更加合适。
如:
StringrawId=“PM-P-09-12-2014-JDJLADD”;
ProductIDproductId=newProductID(rawId);
实体的唯一标识由资源库来创建较为合适
5.1.3持久化机制生成唯一标识
利用持久化机制从数据库中获取标识要比应用程序生成标识慢的多,但我们可以将数据库生成标识事先缓存在应用程序中,比如在资源库中,不过这样在遭遇到系统重启的时候我们就有可能丢掉一部分标识。
标识可以在持久化实体之前生成并赋值,也可以在持久化实体的时候由数据库生成并赋值。
具体的实现还需要看当时的环境。
5.1.4另一个限界上下文提供唯一标识
尽量不使用这种策略,因为我们在更新本地上下文中的实体时还要同时顾及到另一个限界上下文中的实体;
同理在更新了另一个限界上下文中的实体后也要同步本地上下文的实体。
5.1.5委派标识
有时候我们用持久化机制生成的标识(数据库提供的机制),但有可能我们的领域还需要另一种标识,这两者将发生冲突,为了解决这个问题这时候我们应该使用两种标识,一种为领域所用,一种为ORM所使用,为ORM所使用的标识称之为委派标识。
委派标识不属于领域模型,所以我们将其向客户端隐藏,而将其建在实体对象的接口中。
委派标识在数据库中采用long或int类型,作为数据库的主键存在;
领域标识可以不作为数据库主键存在,只作为唯一键存在即可。
5.1.6标识稳定性
程序尽量别对已有的领域标识进行修改。
可以将领域标识的setter方法向客户隐藏起来。
5.2实体及其本质特征
在我们创建实体的过程中有可能会犯以下两个错误:
●我们认为对象就是一组命名的类和在类上定义的操作,除此之外并不包含其他内容。
●通用语言就是一组术语和一些简单的用例场景
实体中必要的属性就是其“本质特征”
当我们在通用语言中找到某个对象需要修改(修改不等于被替换)、需要唯一标识与其他对象区分的时候,那么这个对象就可以认为是实体了。
5.2.1关键行为
在这个类图中我们将一个用户对象分解为四个对象分别是User、Person、ContactInformation、Name。
Person实体的关键行为我们放在了User实体中。
5.2.2角色和职责
建模的一个方面就是发现对象的角色和职责。
通常来说,对角色和职责分析是可以应用在领域对象上的。
一个类对于每一个它所实现的接口来说,都存在一种角色。
如果一个类没有显示的角色—即该类没有实现任何接口,那么在默认情况下它扮演的即是本类的角色。
看下面这段代码
在这段代码中无论是personPrincipal还是systemPrincipal不具有UserPrincipal实体的身份标识,UserPrincipal才是行为的最初执行对象。
这段代码患了对象分裂症:
委派对象(personPrincipal或者systemPrincipal)根本不知道原来被委派对象(UserPrincipal)的身份标识,因此我们无法知道委派对象的真正身份。
一个实体在不同的场景中它的角色不同,行为也会不同。
我们在编写程序时要时刻注意这一点,这样才是面向对象的编程。
例如对象
在引入角色、场景后可以变为
这样Customer就拥有了IAddOrdersToCustomer和IMakeCustomerPreferred两种角色了。
当然,我们也不希望创建一个拥有大量对象的集合。
5.2.3创建实体
创建实体我们可以采用构造函数的方式进行,用构造函数一方面可以用于表明实体身份,另一方面可以帮助客户端更容易的查找该实体,在使用及早生成唯一标识的策略时,构造函数至少需要接受一个唯一标识作为参数。
一般我们在构造函数中采用自封闭的方式创建实体。
创建复杂的实体,我们一般采用工厂的方式进行。
5.2.4验证属性
建议采用自封装的方式来验证属性。
福勒曾经说过“自封装性要求无论以哪种方式访问数据,即使从对象内部访问数据,都必须通过getter和setter方法。
”
实体对象持有值对象,在将一个整体值对象赋给实体对象时,只有当值对象中的所有较小属性得到验证,我们才能保证对整体值对象的验证。
5.2.5验证整体对象
由于验证逻辑需要访问实体的所有状态,有人可能会直接将验证逻辑嵌入到实体对象中,这样是很不好的,因为验证逻辑的变化要比实体对象的变化频繁的多,而且将验证逻辑放在实体对象中也使实体对象承担了太多本不该它承担的工作。
正确的做法是将验证逻辑独立成一个单独的类,将此类放在跟实体对象同一个包下,然后在实体类中调用验证类的验证方法。
实体类和验证类之间的关系最好用规范模式和策略模式来设计。
5.2.6验证对象组合
验证对象组合最好的方式就是将验证过程创建成一个领域服务,该服务通过资源库读取那些需要验证的聚合实例,然后对每个实例进行验证,可以单独验证,也可以和其他聚合联合验证。
5.2.7跟踪变化
跟踪变化最实用的方法是领域事件和事件存储。
技术团队将领域专家所关心的实体状态变化创建成领域事件,事件的名字和属性可以表明发生的事件。
当命令操作完成后,系统将发出这些领域事件,事件的订阅方接收到这些事件后会保存在事件存储中并会执行相应的操作。
6值对象
值对象用于度量和描述事物。
我们应该尽量用值对象来建模而不是用实体对象。
用于描述实体的某个方面而本身没有标识的对象成为ValueObject。
ValueObject所包含的属性应该形成一个整体,其应该被设计成不可变的对象,不要为其分配标识,ValueObject在变更使用时最好是整体替换。
ValueObject的使用分为两种,一是复制、二是共享。
复制与共享究竟该使用哪一种方式取决于实现环境及程序员的经