现在的位置: 首页 > 综合 > 正文

[编程思想]领域模型和缓存应用【一】

2019年08月05日 ⁄ 综合 ⁄ 共 9367字 ⁄ 字号 评论关闭

原文链接(感谢原文作者!)


前几天给部门内部做了一个DDD方面的培训,这篇文章就记录一下培训的主要内容。

 

 软件的目标是什么

软件的目标是快速地响应客户的需求变更,传统的软件开发方式割裂了软件的功能性需求和非功能性需求,首先业务人员分析好需求以后,拿给开发人员进行开发,这样就使得软件的功能性需求是依赖于某一种技术了,甚至有时候还会造成软件系统离开一两个开发人员就不能维护了,这其实都是将功能性需求和非功能性需求分离造成的后果。

采用领域驱动的开发方式,最终系统形成了一个通用的模型,这个模型是完全面向业务的,这个模型是业务人员和开发人员都能容易理解的,同时这个模型也是如实的反映了领域实质的,这样以来软件不是依赖于某种技术,同一个模型可以用不同的技术来实现。与此同时,采用领域模型以后,领域模型是一个对象模型,而这个对象模型是容易理解,容易维护,容易复用,同时加入分布式缓存系统以后,对象模型是具有伸缩性的,因此领域模型在分析之初就将功能性需求和非功能性需求统一在了一起。采用领域驱动设计以后,软件系统的功能性需求和非功能性需求完美的统一了,那么具体都有哪些非功能性的需求呢?

Extendability(扩展性)

任何事物都处于一种发展变化当中,软件当然也不例外,因此一个软件系统必须要良好的可扩展性,当新的需求出现了,或者需求发生变化了,软件如何能跟的上变化,如何能更快的加入新的功能,这些都是一个良好设计的软件应该具有的性质。

2 Maintainability (维护性)

从哲学的角度来说,任何一种事物都是有生命的,软件也不例外,在软件的生命周期当中,难免会出现要对软件进行维护,而一些软件系统由于文档,代码,注释等等的原因,造成了软件的维护下很差,维护成本很高,因此一个好的软件系统必须要要具有良好的维护性。

Reuseability (复用性)

复用的概念可以说已经充斥在我们每个人的日常的生活当中,同样的软件系统也应该有复用性,一个设计良好的软件系统,它的内部各种组件都是良好复用的,在需要一些功能的时候,可以通过已经存在的组件来构造,而不是每个功能都重头来做一遍,这样不仅增大了开发成本,减低了开发的效率,同时这个软件系统的复用性就降得很低。

Scalability(伸缩性){垂直,水平}

软件的可伸缩性是指在软件系统负载变大的时候,软件系统只需要增加更多的资源就可以应对更大的负载,响应更多用户的请求。而软件的伸缩有方向性,通常有横向的和纵向的,横向就是指水平的伸缩性,在负载增多的时候,我们增加更多的逻辑单元,让这些逻辑单元就像是同一个单元一样,比如我们增加更多的Jboss的实例,增加多个PC server等。而纵向来说,就是指垂直伸缩性,垂直伸缩式指对同一个逻辑单元进行增强,比如增加CPU,增加内存,增加更快速的磁盘等等。

在软件的的伸缩性中,垂直伸缩性往往是受限制比较大的,并且成本也比较高的,一个普通的Server,你不可能无限的增加CPU,增加内存等,因此总是有个限制,而水平伸缩,限制就会小很多。但是如何设计我们的软件系统使其更加具有伸缩性,这也是一个大的挑战。而采用领域驱动设计和缓存的方式,就可以提高软件的可伸缩性,更准确来说就是提高软件系统的水平伸缩性。

Performance(性能){多快,多大}

通常我们在理解一个软件系统的性能的时候,我们第一时间都会想到,这个软件系统快不快,好像一个软件系统只要速度快就是性能好,其实这样理解软件系统的性能,存在一定的偏差,速度快只是性能的“多快”的方面,性能还有很重要的一个方面,那就是“多大”,软件系统能支持多少用户,软件系统在支持多少用户量的时候还能保持某一个响应的速度,这就是性能的“多大”的方面。因此在考虑系统的性能的时候,需要从“多快”和“多大”两个方面来考虑。

二 Domain Driven Design(领域驱动设计)

领域驱动设计的概念

1.1 What is the domain model?

首先软件是什么?在人们的脑海中,软件好像就是一种计算的工具,但是这是软件刚刚出现的时候的概念。现在的软件已经不仅仅是一种计算工具,它代表的是一种针对某一个领域的解决方案,软件可以帮助某一个领域来完成特定的工作,软件可以帮助我们处理现代生活中复杂的工作。

理解了软件是什么以后,我们需要清楚一个软件的核心是什么?也许有人会说软件的核心是实现软件的技术,是的,我也承认,软件必须通过某一种技术来实现,但是我这里想说的软件的核心应该是一个模型,是一个与具体技术无关的模型,因为技术的发展日新月异,并且我们的客户也是看不到你到底用了哪种技术来构建软件的,客户关心的是你有没有真正的实现软件的需求,你的软件有没有如实的完成了符合某一个特定领域的工作。而生活中某一个领域相对来说是稳定的,从而某一个领域所对应的模型也应该是相对稳定的。

理解了软件的核心是一个忠实反映软件所解决问题的领域模型后,我们再来说说什么是领域模型。软件的领域模型可以通过好多种方式来描述,而在面向对象的技术变得越来越流行的当今社会,领域模型最合适的描述方式就是通过一个对象模型来描述领域模型。因此就目前来说,领域模型可以理解为反映一个领域实质的对象模型。

因此软件设计是一个艺术的过程,软件设计不能从数学的角度去思考,软件不能通过定理通过公式来表达,软件需要通过一个模型来表达。

1.2 Ubiquitous Language(通用语言)

领域驱动设计引入了Ubiquitous Language的概念,UL是业务专家或者领域专家和开发者采用的通用语言,在讨论中,开发者和领域专家都通过UL进行讨论,这样就避免了领域专家和开发者用不同的术语描述的是同样的概念,从而引起的混淆。

Ubiquitous Language更加侧重于业务和领域方面的术语,而不是技术术语,我们作为开发人员,经常讨论中会引入一些技术方面的术语,这应该是所有开发者的通病,在领域驱动设计中,所有参与项目的人共用统一的通用语言。无论是BA,PL,PM,SE还是开发人员,在讨论的过程统一使用通用语言。

1.3 Bounded Context(边界上下文)

在进行DDD实践的过程中,我们完全可以将系统所有的对象都建模为一个模型,这个模型中包含了系统中所有的对象,这样做可以,但是这样以来就会使得领域模型变得非常的大,同时也增加了领域模型维护和改进的难度,因此需要一种机制来使得领域模型能划分的更细一点,这就是所谓的“Bounded Context"

边界上下文将领域模型划分为一个边界子领域,这样使得大的领域模型划分小的子领域,这样在扩展维护的时候或者在对领域模型进行重构的时候,影响就会变的小。拿大家熟悉的电子商务领域来说,在一个电子商务领域,整个领域模型是很庞大的,因此有必要将其划分为小的子领域,比如在购物的时候,我们有个Shopping的概念,在下订单的时候有"Order"的概念,这些其实就是一个不同子边界上下文,Shopping ContextOrder Context.

2 为什么要引入领域驱动设计?

2.1 目前项目中存在的问题

2.1.1 不注重软件的生命周期  

任何一种事物都是有生命的,这种朴素的哲学观也可以映射到软件系统当中。系统初期也许负载量小,系统运行良好,但是随着用户量的增大,系统的负载也变的越来越大,这个时候如果在系统设计初期没有注重软件的生命周期的话,那么系统就很容易随着负载的增加而宕掉,同时软件的追究不在于满足当前的客户需求,软件更应该追求一如既往的满足客户的需求,如何使得软件能够在新的需求出现的时候快速的响应,并且以较小的成本来完成需求变更响应,这就要求在软件在设计初期就考虑到软件的生命周期。因此如果想让我们的系统能平滑的应对大负载,同时能快速的跟上需求变化,这个时候就需要合适的架构和真正符合领域实质的领域模型。

2.1.2 过分依赖数据库编程

目前很多J2EE的系统都存在过分依赖数据这种现象,每次业务操作都是直接调用dao来完成,这样其实和以前那种存储过程是差不多的,通过这种方式开发,无形中给数据库造成了相当大的压力,而项目伸缩性的最大的敌人也正是数据库。过分依赖数据库最终就造成整个系统压力跑到了数据库里面,随着系统负载的不断增加,数据库的压力将越来越大,最终数据库因不堪重负而宕掉。因此如果过分依赖数据库,那么我还用那些中间件服务器做什么?还用Java做什么?所以一个系统要想有一个好的伸缩性,第一步就是要打破依赖数据库这个盒子,打破数据库这个盒子的目前最合适的方式就是采用领域模型的方式。

2.1.3 面向过程思维

Java是一门面向对象的语言,那么是不是用了Java就表面是面向对象了?完全不是一个概念!目前的很多项目中,其实都是面向过程式开发,大量的业务逻辑都在Service里实现,而领域对象其实完全都是贫血的,没有行为,仅仅是一种数据容器,这样以来每次业务操作都是action-->service-->dao,这其实就是一种面向过程的思维,专业一点,这说白了就是POEAA中的事务脚本模式,这种方式只适用于小项目,大型项目就需要采用领域模型的方式进行。

2.1.4 不能快速响应需求变化

软件的需求是多变的,如何应对这种多变的需求,这对软件开发来说是个大的挑战。而如何应对这种变化呢?我们在具体的开发当中就应该采用敏捷迭代,持续重构和改善的方式来进行,而不能采用传统软件工程中“瀑布式的”这种软件工程方法。在我们公司的价值观里面有一条“拥抱变化,学习成长”,这个拥抱变化的思想非常重要,它正面面对了一个软件开发当中不可避免的问题。

在采用了敏捷迭代,持续重构、改善和拥抱变化的开发方式和思想以后,相当于我们确定了一个总体的软件开发的方式,但是这样还不够,到底我们迭代什么,持续重构和改善什么还没确定,这个时候就正是领域模型发挥作用的时候。在软件需求发生变化的时候,我们积极主动的去拥抱了需求的变化,而同时我们将这种变化如实的反映在领域模型当中,这种改善和重构是为以后更快速的开发做准备,因为随着项目的进行,领域模型已经能越来越真实的反映领域的实质,这样就软件开发速度就会越来越快,因为很多功能在领域模型里面已经完成了,需要的时候只需要复用就可以了。

2.1.5 需求分析和设计不匹配

需求分析人员或者业务人员的职责重点就是从客户那里挖掘出需求,真正理解客户需要什么,客户需要我们的系统是个什么样子,等业务人员提炼需求以后,设计人员根据需求分析文档进行软件的设计,初看这也许没什么问题,但是深入的想下去,我们就会发现这个环节缺乏一个良性的互动,缺乏一个统一的语言。缺乏互动和缺乏统一的语言最终就造成分析和设计脱节,从而延缓了项目开发进度。

所以要想真正跨越分析和设计之间的鸿沟,目前领域建模是非常合适的方式。通过领域模型,分析人员和设计人员在一起讨论,形成一个初步的领域模型,这个过程中,分析人员和业务人员就有一个统一的语言,这样分析人员和设计人员也能更加的理解客户的需求,同时形成的领域模型也能更忠实的反映领域的本质,这样的领域模型的复用性是非常高的,这也就显著的提高了项目开发效率。

2.1.6 不重视对象的生命周期

Java是一门伟大的语言,它内置了垃圾收集器机制,这样是不是就说明我们不需要关注内存中对象的管理了呢?其实我们还是要关注对象的生命周期,系统中的一些对象如果用完了就扔给垃圾收集器,这样势必会造成垃圾收集器的频繁启动,而垃圾收集器的启动在采用不同的垃圾收集策略的情况下是具有非常不同的区别的。因此对象生命周期管理也是开发一个优良的面向对象的软件系统很重要的一项任务。

3 领域模型和架构的关系

在目前J2EE项目中,软件架构主要采用分层架构的思想,而分层带来的好处就是提高软件的可扩展性,可维护以及可伸缩性。J2EE项目传统上划分为3层:表现出,业务层,持久层。

3.1 DDD中分层标准

3.1.1 Presentation Layer(表现层)

表现层负责提供用户的接口,它和传统的表现层的概念是一致的。表现层仅仅是负责接受用户的请求,然后调用应用层层获取领域对象来渲染结果视图,最终进行视图的展现。表现层主要采用MVC模式。

3.1.2 Application Layer (应用层)

应用层定义了软件系统所能做的事情,但是它不负责怎么做,也就是说应用层只定义了"what to do",而不关注"How to do".应用层负责调用具有丰富业务逻辑的领域对象来完成某一次的业务操作。同时应用层还需要负责提供与其它系统进行交互的接口。

应用层不负责保存与业务有关系的状态,它仅仅只是将工作委托给领域对象来完成,虽然应用层不包含业务规则和状态,但是应用层可以包含操作过程的状态,比如事务状态等。

3.1.3 Domain or Model Layer(领域或模型层)

领域层是系统的核心,领域层实现了软件的核心的业务逻辑和业务规则,领域模型就属于这一层。

领域层具体会包括很多重要的对象,这部分将在“领域驱动设计中的关键角色”部分说明。

3.1.4 Infrastructure Layer(基础结构层)

Infrastructure Layer提供了系统技术性的支持,比如持久化访问数据库,消息发送,邮件发送等。

3.2 领域模型在架构中的地位

架构是整个系统骨架,架构是一个水环境,而领域模型是鱼。领域模型即独立于架构又服务于架构的。独立性体现在领域模型可以用于不同的架构环境中,就好像把一条鱼从一个水域移到另外一个水域,它照样可以存活一样,而服务于架构体现在领域模型需要融入具体的架构中,才能构成完整的系统。

架构关注与系统的整体的结构,这个结构不仅会涉及到系统本身的业务,比如系统主要有那些业务模块构成,而且也会涉及到具体的技术实现,比如业务层是采用spring,EJB还是Jdon,持久层是采用hibernate,JPA,IBATIS还是JDBC。而领域模型是完全面向业务的,它不会与具体的技术耦合。因此领域模型和架构的分离还体现了一种思想:分析和设计的时候分离,实现的时候粘合,而到真正运行的时候完全统一的思想。(领域模型在分析和设计的时候是独立于架构,而实现的时候会和具体的架构进行粘合,而真正运行的时候会和具体的架构进行完全的统一),这样EJB分布式组件当初的思想(编码时分离,部署时粘合,运行时真正统一是一致的)。

领域模型是针对于某一个特定领域的,因此每一个不同的领域都会具有不同的领域模型,但是对于相同领域的不同项目,我们可以采用一套相同的领域模型来进行开发。而软件架构是可以在不同的领域进行复用的,比如struts,spring等框架,以及分层的思想等,这些都是可以在不同的领域进行复用,因此领域模型是面向领域的复用,是一种针对特定领域的复用,而软件架构是一种更加宽泛的,更加偏向于技术方面的复用。

4 领域驱动设计中的关键角色

4.1 Entity(实体)

实体具有一个显著的特征,那就是实体都是有(Identity)标识的,我们判断两个实体到底是不是一样的,我们只是根据实体的Identity,两个实体即使其它的属性都一样,但是只要标识不一样,那么这两个实体也是不一样的,比如在软件系统中,有两个Customer对象,这个对象的其它属性都一样(名字,性别,年龄等),但是他们的Identity不一样,那么这两实体就是不一样的。

DDD中的实体和我们一般的软件系统中的实体有什么区别呢?那就是DDD中的Entity是具有丰富的业务行为的,是充血模型的,而不是贫血模型的,像一般的软件系统中,实体往往都只是数据库表中数据容器,没有任何行为,所有的业务逻辑的实现都跑到了service层,而Service不能如实的映射到领域中,因此用它来表达业务也是不适合的。

充血模型和贫血模型,我们在判断实体到底是充血模型还是贫血模型的时候,不能仅仅只从单一实体的行为上来看,要从一个整体的角度来看,单单看一个实体,它是无行为的,但是这个实体属于一个聚合边界的时候,整个这个边界是充血的,因此我们判断到底是充血还是贫血的时候,应该有一个边界的概念,只要边界里面的实体对象整体上业务行为丰富就OK

4.2 Value object (值对象)

J2EE中各种O的概念太多了,比如PO,VO等等,DDD中的值对象与以前的值对象是有区别的。在EJB2.X中有Entity bean的概念,因为Entity bean也是以一种分布式组件,一次每次远程调用都是有开销的,因此SUN公司的工程师们创造了一个值对象的概念,值对象就是Entity bean的数据,它仅仅起到了在系统各层之间传递数据的功能。

DDD中的VO有它自己的含义。DDD中的VO一般都是一些描述性的对象,通常都是对Entity的描述,比如一个SNS型网站,它有PersonalPage(个人主页)的概念,PersonalPage有一些状态,比如最近访客列表,好友列表,新鲜事等等,这些其实都是一种状态信息,我们就可以将其做为PersonalPage的状态,还比如论坛帖子,它也有一些状态信息,比如帖子的回复有多少,最近一次回复是什么时间等,这些也可以封装到一个VO里,当做是帖子的状态对象。

DDD中的VO还有一些不可变的对象,比如软件系统中的Money,Address等对象,这些对象都只是根据值来确定的,只要两个Money对象的值一样,我们就可以在不同的实体里使用,而不需要区分这个Money到底是那个实体的Money.

4.3 Aggregate(聚合)

DDD中的聚合有点类似UML中聚合的概念,聚合代表一些逻辑上联系比较紧密的对象的集合,每一个聚合都有一个Aggregate Root(聚合根)对象,聚合根控制了对聚合内部对象的访问,聚合外部要想访问聚合内部的对象,必须通过聚合根对象来访问。

OrderOrderLine来说,订单Order是聚合根,而Orderline是聚合内部的子对象,聚合外部要想获取Orderline的信息必须通过Order来获取,同时Orderline也是完全属于Order的,Orderline不能独立于Order而存在,系统中不能存在没有OrderOrderline,因此一般在删除Order的时候,Orderline也需要进行删除。

聚合还有一个重要的特性,那就是不变量的约束,聚合根对象要保证自己聚合内部的不变量是一致的,聚合根提供给外界的方法都必须有前置条件和后验条件的约束,比如Order的总价格必须要和每个Orderline加起来的总价格是一致的,而不能发现不一致的状态,这个一致性的维护都要通过Order来进行。

因此聚合最重要的两个特点就是聚合提供了一种组织逻辑上联系紧密对象的一种方式,同时聚合还要保证聚合内部对象生命周期以及聚合不变量的约束。

4.4 Repository(资源库

Repository(资源库)顾名思义,它代表的是放资源的一个仓库,那么它里面到底放什么东西呢?这就是系统的领域模型对象。

Repository提供了访问领域模型对象接口,领域层通过Repository来获取领域对象。

在传统的开发当中,我们非常熟悉Dao的概念,DDD中的RepositoryDao是不同的概念,DDDRepository是完全面向领域对象的,领域层从Repository获得的对象是符合不变量约束的对象,往往这个对象就是Aggregate Root对象,当系统从Repository拿出领域对象的时候,不用再考虑这个对象是不是完整的,它里面的子对象有没有嵌入,它的不变量有没有得到保证,这些在从Repository拿出领域对象之前,Repository都已经帮我们做好了。

Repository还屏蔽了系统底层具体的持久化技术,无论我们底层采用什么样子的存储方式,比如RDBMSXMLFile SystemRepository都提供一致的接口给领域层使用。

4.5 Factory(工厂)

Factory和GOF设计模式中的工厂是类似的,采用DDD后,系统会形成一个完整的领域模型,领域模型里面包含的丰富的领域对象,这些领域对象往往都包含丰富的行为,同时也包含自己的不变量的约束,因此我们可以通过Factory封装领域模型对象创建过程,Factory封装了领域对象的创建逻辑,同时Factory还会保证领域对象创建以后是完整的,是符合不变量约束的。

Factory的引入主要是为了控制领域对象的生命周期,Factory控制了领域对象生命周期的开始,而Repository控制了从创建以后到最后消亡的生命周期。

4.6 Service(服务)

Service是一种大家比较熟悉的概念,在传统的开发方式中,我们的系统中业务逻辑的载体就是它了。但是在DDD中,Service的概念和传统的概念是不同的。

DDD中的Service可以分为两种类型,一种是Application Service(应用层服务),另外一种是Domain Service(领域层服务).

Application Service(应用层服务)关注点不在于系统的业务逻辑,应用层的关注点主要在于系统功能的定义,也就是说应用层服务一般定义了What to do,而不关注“how to do",应用层服务要想完成某一次的业务操作,需要调用领域模型对象来完成。应用服务还涉及到一些与系统级状态有关系的信息,比如应用层服务一般都会涉及到事务控制,安全访问控制以及日志记录等功能。

Domain Service(领域层服务)是属于领域模型中的,它关注与业务逻辑的实现,在系统中有一些行为可能不属于Entity(实体)Value Object(值对象),那么这些行为就要划分到Domain Service里面。

比如在电子商务系统中一般都有ShoppingCart(购物车)的概念,当用户需要查看本次购物的总价格的时候,这个getShoppingCosting职责的实现就不能由ShoppingCart来实现,而相应的会由PriceServiceShopcostingService来实现,因为这些行为会涉及到与系统其它部分或者外部系统的交互,比如PriceService会涉及到与外部价格系统的交互等等。

Domain Service使得EntityValue object对象更加的高内聚和低耦合,这其实也反映了面向对象的设计原则,为了让EntityValue只包含自己应该包含的职责,对于一些本不属于自己的职责,则有专门的Domain Service来实现。

4.7 Domain Event(领域事件)

Domain Event是一种使得领域模型更加高内聚,松耦合的机制,通过引入领域事件,使得核心的领域模型和ServiceRepository解耦,这样使得领域模型能更加真实的反映领域的实质。   

<!--EndFragment-->

抱歉!评论已关闭.