【文章整理】一文看懂Cola架构和DDD

Cola框架学习

偶然了解到cola这个框架(也是架构),接触到了DDD的思想, 为了理解代码,搜阅了很多文章,发现没有一篇能讲的通彻的,因为其中其实牵扯到很多知识,所以将各个方面的内容做了一个汇总,方便学习。

所引用文章路径已经标明,如有侵权,还请联系作者删除

采集参考文章:

领域驱动架构(DDD)建模中的模型到底是什么

领域驱动设计系列 (六):CQRS

复杂度应对之道 - COLA应用架构

应用架构COLA 2.0

应用架构之道:分离业务逻辑和技术细节

对于单表模式的反思

单表功能做多了最容易犯错的地方就是完全的数据库表式思维模式,即任何一个数据库表都 会有对应的展现页面,控制类,service类和dao类,完全一对一映射和调用。在这种思维模式下忽略了最前面思考的对象的本质,在领域模型里面一个核 心就是我们关心的是有明确业务含义的对象,而不是数据库表。数据库表和dao层只是在最终持久化要做的事情而已。

为什么要建模;怎么建模才合理;“领域”模型具体指什么。

必看!此回答是笔者搜索了很多资料后描述的最清楚的一篇。篇幅较长,建议跳转观看。

领域驱动架构(DDD)建模中的模型到底是什么? - dz902的回答 - 知乎 https://www.zhihu.com/question/25089273/answer/233316164

CQRS概述

CQRS是Command Query Responsibility Seperation(命令查询职责分离)的缩写。 世上很多事情都比较复杂,但是我们只要进行一些简单的分类后,那么事情就简单了很多,比如我们把人分为男人和女人,也可以把人分为大人和小孩,还比如,我们说国内和国外,城市和农村。经过一些类似这样的划分,我们的对不同的类就有不同的关注。 这样我们就会有妇女儿童医院专门让女人生孩子,而不会建一个医院让男女都生孩子。

其基本思想在于,任何一个对象的方法可以分为两大类:

命令(Command):不返回任何结果(void),但会改变对象的状态。

查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

CRUD和CQRS对比

CRUD

CRUD (Create, Read, Update, Delete) 增查改删,我们很多系统都是对数据的增查改删。过去我们很多系统比较简单,基本上增加的数据就是你要查询的数据,所以很多时候其实一个简单的Excel就能搞定。 而且增删改查也足够的简单,所以我们很多系统分层后在数据层Repository里仍是对单表的增删改查,这样对不少的系统都符合。

但是,系统规模稍微大一点,我们都知道我们的数据库里的数据模型很难和我们业务层需要的模型一致。 于是我们引入了Domain Model, Repository里就会做Domain Model的来回转换

【文章整理】一文看懂Cola架构和DDD_第1张图片

同时我们在UI层要的数据,往往又和具体的Domain不同,这个时候我们又要定义一个ViewModel. 而这些ViewModel又是组合不同的DomainModel得来。

传统的代码里的问题:

  • 领域里有很多分页和排序,尤其是Repository里
  • 查询的方法里暴露了很多不应该有的领域模型的属性,因为需要组装DTO
  • 如果使用ORM,预加载了很多数据以提高性能,但是占用大量内存,而且需要维护这些数据。
  • 加载组合庞大的数据,比如页面是需要一个名字,我们也会把整个User数据取出来。

重要的原来把数据混在一起,复杂的查询相当难以优化。 尤其是数据库出现大量的Join 系统性能极速下降。

最重要的是我们把读写都放在了一起,显得责任不够清晰,代码也更复杂了一些,比如读数据是不太关心事物的,读数据是不需要验证的,只有写的时候才需要做数据校验,这也比较符合SRP(单一职责),但是用CRUD的思维是我们全都混在了一起。

CQRS

我们仔细看CRUD, 其实可以更简单的分为读®和写(CUD), 我们想想大部分情况都是,一个方法要么是执行一个Command完成一个动作,要么就是查询返回数据。 比如我们回答问题的人不应该去修改问题。

当我们读写分离后,我们对应的代码也会分离。

数据存储

写的一端需要保证事物,所以一般数据存储为第三范式,
读的一端一般都是反范式可以避免Join操作,这样我们只需要把数据存储为第一范式

扩展

大部分的系统里写数据要远远少于读数据,并且一般都是每次修改很少的一部分数据,所以在写这端扩展都不是特别紧迫,读数据基本都远大于写数据的次数, 所以扩展就更重要。 我们很难建立同一个Model 既能给写数据和读数据公用而且能够保证性能都比较好的。

查询端

查询端由于只是读数据,那么所有的方法应该都是返回数据,而且返回的数据就是界面直接需要的DTO, 这样可以减少传统的方法中把DomainModel映射为ViewModel或者DTO. 同时可以减少传统的领域里的一些混乱。

写端

由于把读分离出去,所以我们就只关注写,那么我们写这一段需要保证事物,数据输入的验证,另外一般写这一端都不需要及时的看到结果,所以大部分都需要一个void方法就可以,那么让我们系统异步就更加方便。这样使系统的扩展性大大增强。

代码更容易集中处理

当我在一些系统中使用CQRS后,很多地方代码大大简化,比如我所有的写操作都是一个Command, 那么我定义一个UICommand, 让所有的Command集成这个,那么我可以在这个UICommand里做一些通用的处理,比如Validation

同时我只需要定义一个CommandBus, 然后把对应的CommandBus分发到对应的Handler里(我前面几篇有实例代码),那么代码的耦合度大大降低。

代码分工协作更容易

由于读这一端直接读数据,而且对数据库没有任何操作,那么我们可以根据UI定义对应的DTO, 那么开发的时候我们可以用Mock数据,至于数据怎么存的,那么我们随后只要添加一层Thin Data Layer即可,实际上当我们使用CQRS后,很多时候我们把数据保存的时候都直接保存为Denormalize的,那么从数据里直接查询单表的数据就可以拿到页面需要的数据,大大提升读取数据的性能,同时代码也会极其的简化,开发读这一段代码的开发人员甚至都不需要对业务有太多了解。

DDD(Domain-Driven Design 领域驱动设计)

准确的说DDD不是一个架构,而是思想和方法论,关于如何领域建模的详细请参看我另一篇文章领域建模。所以在架构层面我们并没有强制约束要使用DDD,但对于像我们这样的复杂业务场景,我们强烈建议使用DDD代替事务脚本(TS: Transaction Script)。因为TS的贫血模式,里面只有数据结构,完全没有对象(数据+行为)的概念,这也是为什么我们叫它是面向过程的原因。

然而DDD是面向对象的,是一种知识丰富的设计(Knowledge Rich Design),怎么理解?,就是通过领域对象(Domain Object),领域语言(Ubiquitous Language)将核心的领域概念通过代码的形式表达出来,从而增加代码的可理解性。这里的领域核心不仅仅是业务里的“名词”,所有的业务活动和规则如同实体一样,都需要明确的表达出来

领域实体

现实世界中的事物在软件世界中可以被模拟成一个对象:该事物在现实世界中被赋予什么职责,在软件世界中就被赋予什么职责;在现实世界中拥有什么特性,在软件世界中就拥有什么属性;在现实世界中拥有什么行为,在软件世界中就拥有什么函数;在现实世界中与哪些事物存在怎样的关系,在软件世界中就应当与它们发生怎样的关联。这正是面向对象编程的核心思想,也是DDD中寻找领域实体的核心思想。

【文章整理】一文看懂Cola架构和DDD_第2张图片

假如现在你需要设计一个中介系统,一个典型的User Story是“小明去找工作,中介让他留个电话,有工作机会就会通知他”。我们要如何寻找该业务中的关键领域实体呢?一个简单的方式就是“找名词”,分析这些名词,不难得到以下可能成为实体的候选项。

● 小明:一个求职者。

● 电话:求职者的相关信息,可以是一个属性。

● 中介:可以拆解为中介公司和中介公司的员工两个概念。

● 工作机会:对于中介系统来说,工作机会应该是最关键的实体之一。

● 通知:作为名词是一个实体,但是作为一个动词是在暗示我们可以使用Notify。

是的,对于这个简单的User Story,这样分析就可以了。当然,随着更多的Story被加入,我们会补充更多的实体,比如增加了“中介费是按照小明第一个月工资的30%收取”,那么就可能要引入“订单”和“支付”等实体。

聚合根

聚合根(Aggregate Root)是DDD中的一个概念,是一种更大范围的封装,会把一组有相同生命周期、在业务上不可分割的实体和值对象放在一起,只有根实体可以对外暴露引用,这也是一种内聚性的表现。

确定聚合边界要满足固定规则(Invariant),是指在数据变化时必须保持的一致性规则,具体规则如下。

● 根实体具有全局标识,最终负责检查规定规则。

● 聚合内的实体具有本地标识,这些标识在Aggregate内部才是唯一的。

● 外部对象不能引用除根Entity之外的任何内部对象。

● 只有Aggregate的根Entity才能直接通过数据库查询获取,其他对象必须通过遍历关联来发现。

● Aggegate内部的对象可以保持对其他Aggregate根的引用。

● Aggregate边界内的任何对象在修改时,整个Aggregate的所有固定规则都必须满足。

仍以银行转账的例子来说明,如图5所示,账号(Account)是客户信息(CustomerInfo)Entity和值对象(Address)的聚合根,交易(Tansaction)是流水(Journal)的聚合根,流水是因为交易才产生的,具有相同的生命周期。

【文章整理】一文看懂Cola架构和DDD_第3张图片【文章整理】一文看懂Cola架构和DDD_第4张图片

领域服务

有些领域中的动作是一些动词,看上去并不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单地把它们合并到某个实体或者值对象中。**当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务。**这样的对象不再拥有内置的状态,其作用仅仅是为领域提供相应的功能。Service往往是以一个活动来命名,而不是Entity来命名。

例如在银行转账的例子中,转账(transfer)这个行为是一个非常重要的领域概念,但是它发生在两个账号之间,归属于账号Entity并不合适,因为一个账号Entity没有必要去关联它需要转账的账号Entity。在这种情况下,使用MoneyTransferDomainService就比较合适了。识别领域服务,主要看它是否满足以下3个特征。

(1)服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。

(2)被执行的操作涉及领域中的其他对象。

(3)操作是无状态的

分层架构设计

DDD中将系统分为UI层,应用层,领域层以及基础设施层。

【文章整理】一文看懂Cola架构和DDD_第5张图片

image.png

上图中,应用层是很薄的一层,因为它只负责接收UI层传来的参数和路由到对应的领域模型,它不负责处理具体的业务逻辑。系统的业务逻辑放在了领域层中,所以,领域层在系统架构中占据了很大的面积。

在层级结构中,上层模块调用下层模块提供的服务,这里就会存在一种依赖关系,Rebort C. Martin提出的依赖倒置原则大致是如下:

上层模块不应该依赖于下层模块,两者都应该依赖于抽象;
抽象不应该依赖于实现,实现应该依赖于抽象;

这是一个面向接口编程的思想,抽象说的是抽象类或接口,实现就是具体实现了这些抽象的实现类。翻译成白话文是这样的,上下层之间应该通过接口来通讯,接口定义的位置就决定了上下层的依赖关系是否倒置。比如Application层和Domain层进行通讯,接口与接口的实现类都定义在Domain层中,这是正常的面向接口编程,不存在倒置关系。而Domain层和基础设施层进行通讯时,原本是Domain层去依赖基础设施层,如果我们将接口定义在Domain层,而实现类定义在基础设施层,那么,基础设施层就将依赖Domain层,这就是“倒置”这个词的来由。实际上,我们在做这样分层架构设计时,都是将接口定义在Domain层的。

职责划分

  • App层主要负责获取输入,组装context,做输入校验,发送消息给领域层做业务处理,监听确认消息,如果需要的话使用MetaQ进行消息通知;
  • Domain层主要是通过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,然后调用下层Repository做持久化处理;
  • Infrastructure层主要包含Repository,Config,Common和message,Repository负责数据的CRUD操作,这里我们借用了盒马的数据通道(Tunnel)的概念,通过Tunnel的抽象概念来屏蔽具体的数据来源,来源可以是MySQL,NoSql,Search,甚至是HSF等;Config负责应用的配置;Common是一写工具类;负责message通信的也应该放在这一层。

你可能感兴趣的:(工具使用,Java)