不积跬步,无以至千里。 不积小流,无以成江海。

转载自:Jiangzhou.bo

本文总结常用开发模式,简述DDD领域驱动设计,基于DEMO整理出后台架构模式。

分层模式

从大的范围来分,软件可以分为两个层次:前端和后台。前端负责与用户进行交互,负责接收和校验用户输入,并向用户反馈输出,其业务操作是委托给后台来实现的。我们平常见到很多分层架构模式,核心目的都是分层、解耦。

MVC模式

MVC(Model-View-Controller),即数据模型-视图-控制器,MVC 是开发客户端最经典的设计模式。MVC这个概念最早出现在桌面客户端上面,是C/S里面的C,在Web开发中得以发扬光大,实际上,移动App中也几乎都是MVC模式的。

在客户端开发中,Controller管理用户输入、输出和图形界面。数据来自Model部分,Model实际上集中管理了业务数据,是通向后台系统的通道。这使对同一种业务数据展现多种图形界面成为了可能,Controller存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。

MVC模式

后端最典型的MVC就是JSP + servlet + javabean的模式。当用户发出一个请求后,这个请求会被控制器Servlet接收到;Servlet将请求的数据转换成数据模型JavaBean,然后调用业务逻辑模型JavaBean的方法,并将业务逻辑模型返回的结果放到合适的地方,比如请求的属性里;最后,根据业务逻辑模型的返回结果,由控制器来选择合适的视图(JSP),由视图把数据展现给用户。

典型的MVC模式

在很多开源框架中也有MVC设计的体现,如Struts2、SpringMVC等。就单从SpringMVC框架来说,DispatcherServlet是前端控制器,是整个流程控制的中心,由它调用其它组件处理用户的请求,相当于MVC的Controller;Handler(即我们开发的Controller)是继DispatcherServlet前端控制器的后端控制器,Handler对具体的用户请求进行处理,并返回ViewModel;最后由ViewResolver负责将处理结果生成View视图。

在Javaweb开发中,MVC框架充当了UI层和业务逻辑层的适配器的作用,MVC框架实现了UI层和业务逻辑层最大程度的分离。

SpringMVC

三层架构

现在开发中常用的分层模式就是三层架构,这也是比较传统的一种架构模式。通常意义上的三层架构就是将整个业务应用划分为:表现层(UI)、业务逻辑层(BLL)、数据访问层(DAL)。区分层次的目的即为了“高内聚,低耦合”。

表现层:负责与用户进行交互;业务逻辑层:主要是针对具体的问题的操作,也可以理解成对数据层的操作,对数据业务逻辑处理;数据访问层:直接操作数据库,针对数据的增、删、改、查等。

三层架构

对应到开发中,即常用的 Controller – Service – Dao 三层。Controller层为控制层,用来接收用户的请求,不会涉及太多的业务处理操作,会做一些简单的数据校验,业务处理完毕返回数据模型或视图。Controller层中拥有某一个Service层的引用,但凡涉及到业务处理,就交给Service层来操作。Service一般持有某一个或几个Dao层的引用来对数据做处理。一般来说,Service层和Dao层中,都是直接存放的接口类,然后有一个包放所有接口的实现类,impl就是指每个接口对应的实现类。

联系这三层之间的就是实体对象,比如,使用一个DTO映射数据表,Dao层返回的数据对象(DO)、Service层处理的业务对象(BO)、Controller层返回给用户的展示对象(VO)、甚至远程接口数据对象(DTO)都由一个DTO对象完成,这种模式一般在中小型项目中用得比较多,毕竟分层过多会引入复杂性。

DTO

三层架构与MVC

我们经常将三层架构与MVC混为一谈,但是它俩并不是一个概念。三层架构是一个分层式的软件体系架构设计,它可适用于任何一个项目。MVC是一个设计模式,它是根据项目的具体需求来决定是否适用于该项目。三层架构的着重点是“高内聚,低耦合”,MVC的目的则是实现Web系统的职能分工,即职责划分。但它们的总体目的是一样的,都是为了解耦。

三层架构的表现层即客户端,一般是基于MVC模式开发,使用诸如AngularJS、Vue、React等前端框架。数据交互使用JSON。

SERVER-CLIENT

DDD 领域驱动设计

DDD(Domain
Driven Design),领域驱动设计,作为一种软件开发方法,它可以帮助我们设计高质量的软件模型。在正确实现的情况下,我们通过DDD完成的设计恰恰就是软件的工作方式。在实施DDD时,设计就是代码,代码就是设计。

DDD中重要的概念

通用语言(UL)通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员,可以看成业务顾问和技术顾问。我们使用通用语言来捕捉特定核心业务领域中的概念和术语,比如店铺运营、商场促销。

限界上下文(BC)限界上下文是一个显示的边界,领域模型便存在于这个边界之内。这个边界之内的每种领域术语、词组或句子就组成了通用语言,都有确定的上下文含义。但是,限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。限界上下文中可以包含模块、聚合、领域事件、领域服务等基础部件,限界上下文应该足够大,以能够表达它所对应的整套通用语言。

在使用Java时,限界上下文可以看成IDE中的一个工程项目;顶层包名通常表示界限上下文中顶层模块的名字。我们可能将一个限界上下文放在一个jar或者war文件中。对于大型模型,可以将松耦合的领域模型放在不同的jar文件中,这样我们可以按照版本号对领域模型进行单独部署,这其实就可以看成单独的组件、微服务了。

通用语言和限界上下文同时构成了DDD的两大支柱,并且他们是相辅相成的。限界上下文和通用语言存在一对一的关系。

领域:Domain,即一个组织所做的事情以及其中包含的一切。当我们为某个组织开发软件时,所面对的便是这个组织的领域。一个领域被分为若干子域,几乎所有软件的领域都包含多个子域。

子域:我们可以按照实际功能将领域中交织的模型划分成逻辑上相互分离的子域,从而在一定程度上减少系统的复杂性。子域不一定要包含很多功能,简单的子域可以以模块的形式存在。子域又分核心域、支撑子域、通用子域。

核心域:是整个业务领域的一部分,也是业务成功的主要促成因素,系统核心竞争力的体现。从战略层面上讲,企业应该在核心域上胜人一筹。

支撑子域:支撑子域专注于业务的某个方面。

通用子域:通用子域被用于整个业务系统。像用户和权限就可以放到通用子域。

领域模型:领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。领域模型在限界上下文中完成开发,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。

问题空间:在问题空间,我们思考的是业务所面临的挑战。问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域,问题空间是核心域和其它子域的组合。

解决方案空间:在解决方案空间,我们思考如何实现软件以解决问题空间的业务挑战。解决方案空间包括一个或多个界限上下文,即一组特定的软件模型。

上下文映射图:

由多个界限上下文和子域组成的表示当前单个领域或者多个领域之间的集成关系图。上下文映射图主要帮助我们从解决方案空间的角度看待问题。

业务领域

贫血领域对象:指领域对象主要是些公共的getter/setter方法,几乎没有业务逻辑,主要就是用来容纳属性值的对象,比如我们之前使用的DTO。这种对象一般不叫领域对象,只是将关系型数据库中的模型映射到了对象上而已。反之,充血模型就是DDD中的领域模型。

贫血症导致的失忆症:例如,有一个保存店铺的功能,saveStore(Store dto),Store关联有合同、员工、经营证照等,现在的开发模式中,仅仅修改了店铺的名称或者备注属性,会调用这个保存店铺的方法;在新建店铺时,新增了合同关系、店铺员工,也会在saveStore里保存合同关系、店铺员工。这样一来,我们并没有正确的使用saveStore方法,他的职责范围过大,业务意图不明确了,方法的实现本身就增加了潜在的复杂性,时间久了,我们可能会被这段代码搞得一头雾水。说白了就是业务过于集中,不同模型之间耦合度过高,甚至分不清职责边界,导致后期无法维护。

DDD架构

DDD四层架构

DDD四层架构

DDD系统所采用的传统分层架构,核心域位于领域层,上层为用户界面层和应用层,下层是基础设施层。分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。分层架构有严格分层架构和松散分层架构,严格分层架构中,某层只能与直接位于其下方的层发生耦合;松散分层架构则允许任意上层与任意下层发生耦合。由于用户界面层和应用服务通常需要与基础设施打交道,许多系统都是基于松散分层架构的。

用户接口层:这里指的用户可以是另一个计算机系统,也可以是使用用户界面的人。它不应该包含领域或业务逻辑,如果用户界面使用了领域模型中的对象,那么此时的领域对象仅限于数据的渲染展现,可以使用展现模型对用户界面和领域对象进行解耦。

应用服务:应用服务位于应用层中,用户界面和面向服务端点都会将操作委派给应用服务。应用服务是很轻量的,主要负责用例流的任务协调,本身并不处理业务逻辑,可以用于控制事务和安全认证,或者向其他系统发送基于事件的消息通知,还可以用于创建邮件以发送给用户等等。如果应用服务过于复杂,就要考虑领域逻辑是否已经渗透到应用服务中了。有时,应用服务被设计成将用户界面完全地隔离于领域模型,此时,应用服务中的方法签名中将只出现最基本的数据类型(int,long,double,String等),有可能还有DTO。在不使用领域对象返回时,我们避免了依赖和耦合,此时我们可以使用DTO,并提供转换器,虽然会导致一定的内存耗费,但避免了领域对象与不同系统间的耦合。

领域服务:领域服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不是实体(领域对象)的职责时,最好的方式就是使用领域服务,我们应该尽量避免在实体中使用资源库。要避免所有的业务逻辑都位于领域服务中,否则可能导致贫血领域模型。一般来说,我们不需要为领域服务设计独立的接口,直接使用实现类即可,如果确实针对不同的租户有不同的实现标准,可以使用独立接口。因为通常情况下,领域服务总是和领域密切相关,并且不会有技术性的实现,或者不会有多个实现,此时采用独立接口便只是一个风格上的问题。虽然独立接口对于解耦来说是有用处的,此时客户端只需要依赖于接口,而不需要知道具体实现。但是,如果我们使用了依赖注入或工厂,即便接口和实现类是合并在一起的,我们依然能达到这样的目的。

基础设施层:在传统的分层架构中,基础设施层位于底层,持久化和消息机制便位于该层中,包括持久化、消息、邮件等。可以将基础设施层中所有的组件和框架看作是应用程序的底层服务,较高层与该层发生耦合以重用技术上的基础设施。

总的来说DDD就是从以数据库为中心过度到以领域模型为中心,将侧重点从效率改变为维护。从长远的角度看,以领域模型为中心的设计更加清晰,也是一种更忠实于领域抽象的实现,因而可维护性更高。

六边形架构(端口与适配器)

六边形架构

六边形架构也称为端口与适配器。对于每种外界类型,都有一个适配器与之对应。在这种架构中,不同的客户端通过“平等”的方式与系统交互。该架构中存在两个区域,分别是“外部区域”和“内部区域”。在外部区域中,不同的客户(系统、用户等)均可以提交输入;而内部的系统则处理数据相关。为了保证领域模型所在的应用程序的干净简洁和自治性,各种适配器作为防腐层在整个程序的最外层保护着当前的界限上下文不受外部入侵。

从图中可以看出,每种类型的客户都有他自己的适配器,该适配器用于将客户输入转为为程序内部API所能理解的输入。六边形的每条不同的边代表了不同类型的端口,可以将端口想成是HTTP,而将适配器想成Controller用来请求的类。新的客户只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行了。对于每种特定的输出,都有一个新建的适配器负责完成相应的转化功能。

CQRS——命令和查询职责分离

从资源库中查询所有需要显示的数据是困难的,特别是在需要显示来自不同聚合类型与实例的数据时。我们需要从不同的资源库获取聚合实例,然后再将这些实例数据组装成一个数据传输对象(DTO)。

CQRS强调一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。如果一个方法修改了对象的状态,该方法便是一个命令,它不应该返回数据,这样的方法应该声明为void。如果一个方法返回了数据,该方法便是一个查询,此时它不应该通过直接或间接的手段修改对象的状态,这样的方法应该以其返回的数据类型进行声明。

CQRS指导我们将领域模型中包含命令和查询的聚合拆分开,将那些纯粹的查询功能从命令功能中分离出来,最终分为命令模型和查询模型。

CQRS架构本身只是一个读写分离的思想,实现方式多种多样,比如数据存储不分离,仅仅只是代码层面读写分离,也是CQRS的体现;数据存储的读写分离,C端负责数据存储,Q端负责数据查询。这种架构方式有待深入研究。

CQRS

DDD总结

微服务架构

在微服务架构实践中,人们大量地使用了DDD中的概念和技术:

  • 微服务中应该首先建立UL,然后再讨论领域模型。

  • 一个微服务最大不要超过一个BC,否则微服务内会存在有歧义的领域概念。

  • 一个微服务最小不要小于一个聚合,否则会引入分布式事务的复杂度。

  • 微服务的划分过程类似于BC的划分过程,每个微服务都有一个领域模型。

  • 微服务间的集成可以通过ACL。

  • 微服务间最好采用Domain Event(领域事件)来进行交互,使得微服务可以保持松耦合。

DDD所带来的业务价值

  • 你获得了一个非常有用的领域模型

  • 你的业务得到了更准确的定义和理解

  • 领域专家可以为软件设计做出贡献

  • 更好的用户体验

  • 清晰的模型边界

  • 更好的企业架构

  • 敏捷、迭代式和持续建模

  • 使用战略和战术工具

DDD所带来的挑战

  • 为创建通用语言腾出时间和精力

  • 持续地将领域专家引入项目

  • 改变开发者对领域的思考方式