架构模式

一般来说,企业应用指的是大型系统

企业应用的特点:

事件驱动架构

两种模式

Mediator

2021106152652

Broker

202110615598

两种模式的processor都是单一职责的最小执行单元

微内核架构

2021107221455

系统核心的作用在于资源封装与插件规范定义

插件为在核心提供的接口上实现其单一的功能 插件之间应避免依赖 不能影响核心

优点

缺点

设计关键点

系统核心

核心的功能为MVP 所有核心能实现的接口都要经过核心

开放规范

注册规范

标识、功能、位置、依赖、权限

通信机制

插件装载

领域逻辑组织

事务脚本

使用过程来组织业务逻辑,每个过程处理来自表现层的单个请求

classDiagram   class RecognitionService {      +Money recognizedRevenue(long contractNumber, Date asOf)      +void calculateRevenueRecognitions(long contractNumber)   }      RecognitionService --> Database

优点

缺点

事务脚本的组织

当业务逻辑变得越来越复杂时,这一模式很难继续保持良好的设计,许多问题本身是简单的,一个简单的解决方案可以加快开发速度

领域模型

使用面向对象的方法,合并了行为和数据

classDiagram   class Contract {      +recognizedRevenue(Date asOf)      +calculateRecognitions()   }   class Product {      +calculateRecognitions(Contract contract)   }   class RecognitionStrategy   class CompleteRecognitionStrategy   Contract --> Product : *   Product --> Database   Contract --> Database   Product --> RecognitionStrategy : 1   RecognitionStrategy <-- CompleteRecognitionStrategy

开销来源于使用复杂以及数据源的复杂,还要面对将领域模型映射到数据库的问题

领域模型组织

当使用领域模型时,使用数据映射器有助于保持领域模型与数据库的独立性

领域模型的要点在于隐藏数据库的存在,使其对于上层不可见

表模块

围绕表组织领域逻辑,处理数据库中表或视图中所有行的业务逻辑的一个封装

classDiagram   class Contract {      +CalculateRecognitions(ID)   }   class Product {      +GetProductType(ID)   }   class RevenueRecognition {      +Insert(ID, amount, date)      +RecognizedRevenue(contractID, date)   }   Contract --> Database   Product --> Database   RevenueRecognition --> Database

表模块与事务脚本的区别在于表模块的所有操作都是围绕表来进行,而事务脚本则是围绕事务过程来进行。

表模块组织

表模块以一个类对应数据库中的一个表来组织领域逻辑,仅使用一个单一实例,表模块很大程度依赖于以表方式组织的数据

服务层

通过服务层提供一组可用的操作集合给外部使用

服务层定义了应用程序的边界和从接口客户层角度所看到的的系统

sequenceDiagram  数据加载器 ->> 服务层: 访问  用户界面 ->> 服务层: 访问  系统集成入口 ->> 服务层: 访问  服务层 ->> 领域层: 访问  领域层 ->> 数据库: 访问

业务逻辑的种类

实现

服务识别与操作

服务层操作的起点是用例模型以及用户界面

如果系统只有一种用户,那可能不需要使用服务层

定义服务层的考虑就是为了复用

与关系数据库的映射

为了保证对领域对象的修改能及时存储到数据库,需要考虑如下问题:

  1. 标志映射,保证相同的对象只被加载一次
  2. 延迟加载,当对象附带着引用的对象,在需要时才加载

表数据入口

一个实例代表处理一张表中所有的行,通常是无状态的

interface Person {    RecordSet find(int id);    RecordSet findWithXXX(...);    void update(...);}

表数据入口可能是最简单的数据库接口模式

行数据入口

一个实例代表一条记录 内存对象的数据与数据库操作混杂在一起会带来一些麻烦 如不好测试 并会增加复杂度

class Person {    name,age;    insert();    update();}interface PersonFinder {    Person find(...);}

活动记录

一个包装表或视图中某一行的对象,封装了对数据库的操作访问

class Person {    name,age;    insert();    delete();    bool isAudlt();}

活动记录的本质是一个领域模型

活动记录的数据结构应该与数据库完全吻合

活动记录与行数据入口的区别在于行数据入口只有数据访问,而活动记录封装了一些逻辑

数据映射器

随着ORM框架的发展,前面3种方式已逐渐过时,使用数据映射器的方式可以很好地处理大型应用下的数据源使用

在对象和数据库之间的一个中间层,数据映射器自身不被领域层所察觉

interface PersonMapper {    Person select(...);    update(Person);}

当需要分离对象与数据库时,使用数据映射器

元数据模式

可以通过表结构为代表的元数据来自动生成代码,这在一些快速开发框架中很常见

数据库连接

涉及到数据库肯定也涉及到数据库连接,对于连接的管理,可以采用如下方式

web表现层

模板视图

以 jsp php 为代表的模板文件,通过在HTML标记一些数据,来让处理器渲染

缺点在于,很容易被插入复杂的逻辑,变得难以测试

转换视图

如 json 为代表

转换视图把领域数据作为输入,HTML作为输出

与模板视图的区别是转换视图侧重于数据的输入,而模板视图更侧重于输出

两阶视图

  1. 生成一个逻辑视图
  2. 再将逻辑视图对应到html

类似于编译,把业务数据转换为一种中间表示,再从中间表示渲染视图,两步视图的价值来源于分离了第一阶段与第二阶段,使改变更加容易

并发模式

在应用于数据库的并发处理中,本质问题是

  1. 更新丢失:多个工作单元对同一个数据进行修改,导致数据不一致
  2. 不一致读:读取数据时,数据可能被其他工作单元修改,导致两次读数据不一致

工作单元执行在执行语境中,执语境可以是线程,也可以是进程

为了保证并发安全,有一些方案:

乐观离线锁

使用冲突检测与事务回滚来防止事务冲突

通过版本号来实现

sequenceDiagram  session1 ->> 数据库: 获取用户1  数据库 ->> session1: 返回用户1 版本号1  session1 ->> session1: 修改用户1  session2 ->> 数据库: 获取用户1  数据库 ->> session2: 返回用户1 版本号1  session2 ->> session2: 修改用户1  session2 ->> 数据库: 提交 用户1 版本号 1  数据库 ->> session2: 修改用户1成功 版本号2  session1 ->> 数据库: 提交 用户1 版本号 1  数据库 -->> session1: 失败 版本号不一致
UPDATE users WHERE id = 1 AND version = 1;

这种乐观的离线锁是针对具体领域的解决方案

悲观离线锁

每次只允许一个会话访问数据

sequenceDiagram  par 事务边界    session1 ->> 数据库: 获取用户1    数据库 ->> session1: 返回用户1  end  par 事务边界    session2 ->> 数据库: 获取用户1    数据库 -->> session2: 失败 用户1被锁住  end  session1 ->> session1: 修改用户1

尽可能早检测出冲突

锁的类型:

锁管理:如何管理锁与锁的持有者?实现尽可能简单,可以使用散列表映射锁及锁的持有者

粗粒度锁

用锁锁住一组相关的对象,DDD中的聚合根就可以代表是锁的入口点

隐含锁

将加锁的任务交给父类或者框架,避免繁琐的客户编程加锁释放锁导致出现的问题

会话保存

无状态服务不需要在服务端存储会话信息

存储会话信息的一些方法:

客户会话状态

将会话状态保存在客户端

这样服务器就可以是无状态的 可以构建性能强大的服务器集群

为了避免安全问题,将会话状态保存在诸如Cookie等客户端数据上的时候,需要对其加密,如JWT就是其中的一个代表,但这样会带来一定的性能损失

另外一种方式是SessionId为代表的用来保存标识号的技术,这种方式通过一个散列的随机字符串来标识用户,但这样服务器就不再是无状态了,除非引入统一Session服务器,否则服务器还是必须得存储用户的状态

服务器会话状态

将会话状态保存到服务端

数据库会话状态

将会话状态保存到数据库中

会话迁移

会话可以在服务器集群之间转移

分布式

进程内的过程调用非常快,而远程调用则涉及网络延迟和数据序列化等开销

何时必须使用分布对象:

注意远程调用的边界:明确哪些操作可以在本地执行,哪些必须远程调用

许多现代分布式系统中,XML和HTTP被广泛用于数据交换和远程过程调用

远程外观

对细粒度接口对象进行封装,提供粗粒度接口,提高网络传输效率

进程内调用的开销比进程外的小

远程外观的设计都是基于特定客户的需要

数据传输对象(DTO)

传输数据的对象

一般都只用在跨进程的调用当中,跟现在所使用的DTO基本可以等同为同一个东西,现在的DTO也广泛在系统各层之间传输数据使用

DTO中的域应该都是非常原始和简单的,主要是要求可被序列化

如何序列化

组装器模式

组装器对象负责将领域对象转为DTO

classDiagram  class Assembler {    PersonDTO createDTO(Person)    Person createEntity(PersonDTO)  }  Person <.. Assembler  PersonDTO <.. Assembler