Microsoft.NET 企业级应用 架构设计
1.今天的架构师和架构
- 在软件里,架构这个术语恰到好处地指代为客户构建系统。
- 系统存在于环境之中,而环境则通过驱动一系列开发和运维的决策来影响系统的设计。
- 系统的使命可以通过一组需求来描述。这些需求最终推动系统架构的形成。
- 功能性需求定义了软件该有的功能。功能通过输入、行为、输出来描述。其主要问题在于描述期望行为。
- 非功能性需求是指利益相关者明确提出的系统特性(可伸缩性、安全性、可访问性等)。
- 一般来说,非功能性需求必须和功能性需求同时确认。后者产生软件规范;前者则在实现策略上提供帮助,并在技术决策上给予启发。而在很多情况下,功能性需求也会影响系统架构的决策和相关方面,并且往后难以更改。
- 瀑布模型和迭代开发
- 业务逻辑的设计有几种方案:事务脚本,表模块,领域模型,对命令和查询进行分离的领域模型,以及事件溯源等。
- 项目经理:负责选定方法学、安排工作、跟踪进度、回报情况,以及充当技术人员和业务人员之间的有效桥梁。
- 整体设计要与企业目标和需求保持一致。特别的,整体设计是由需求驱动,而不是驱动需求。
2.为成功而设计
- 沟通问题的根源是业务人员和开发者使用不同的词汇,使用和期待不同精度的描述。
- 针对代码,而不是针对写代码的人。但通过写代码的人来尝试改善代码。
- 持续改变时描述现代软件项目动态的有效方式。
- 作为一名架构师,架构和设计重构都是关键工具。架构师无法控制历史以及业务场景和现实世界的发展。架构师需要一直做出调整,避免重新构建。
- 去控制真实存在的复杂性,而不是创造原本没有或不该有的复杂性。
3.软件设计原则
- 所有编程都是维护编程,因为你很少写原创代码。只有你在最初的10分钟里键入的代码是原创的。仅此而已。
- SOLID:单一责任原则(SRP)、开放封闭原则(OCP)、里氏代换原则LSP、接口分离原则(ISP)、依赖反转原则(DIP)。
- 高内聚,低耦合。
- 架构师的设计应该解决手头上的问题、但也要足够通用,可以解决将来的问题和需求。
- 使用继承有一些安全的方式(例:只向派生类添加代码),但出来的对象图总是很难反映现实世界里德默写领域的整个模型。
- 单一责任原则:一个类有且只有一个改变理由。
- 开放封闭原则:模块应该对扩展开放,但对修改封闭。
- 里氏代换原则:子类应该可以替换它们的基类。(派生类不能限制基类的执行条件)。
- 接口分离原则:不应该使用胖接口(不应该强制客户以来于它们不用的接口)。
- 依赖反转原则:高级模块不应该依赖于底层模块。而这都应该依赖于抽象。
- 处理依赖的模式:
- 服务器定位模式
- 依赖注入模式
- 编码向量
- KISS(Keep It Simple,Stupid):不必要的复杂性对于给定的系统需求而言都是附加的逻辑。
- YAGNI(You Ain’t Gonna Need It):实现需求上没有提到的任何功能都是有问题的。
- DRY(Don’t Repeat Yourself):避免保持系统不同部分同步的麻烦。一块代码只有一个明确的实现是重构的首要目标
- 说,别问!
- 每个模式描述在我们的环境里重复出现的问题,然后描述这个问题的核心解决方案,你可以使用这个解决方案无数次,每次使用它的方式都不一样。
- 设计模式只是提供帮助。有一个问题,然后去把问题匹配到设计模式。理解问题,然后对它进行泛化。
- 模式对于解决方案来说并不是附加价值,它们的价值帮助作为架构师或开发者寻找解决方案。
- 重构可以总结为两点:
- 持续代码的编辑改善可读性和设计。
- 执行更加进取的结构调整,使现有代码符合特定模式。
##4.编写优质软件##
- 可测试性的原则:
- 控制性:测试者在多大程度上可以给正在测试的软件提供固定的输入数据。
- 可见性:观察正在测试的软件的当前状态以及它所产生的任何输出能力。(在方法执行后验证后置条件的事情)。
- 简约性:简单和及其内聚的组件非常适合测试。
- 软件测试会出现在各个层次。
- 单元测试:检查软件的单个组件是否满足功能性需求。
- 集成测试:检查软件是否兼容环境和基础设施以及两个或多个组件是否协同工作。
- 验收测试:检查完成的系统是否满足客户需求。
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyMath.Model;
namespace MyMath.Model.Tests
{
[TestClass]
public void TestIfFactorialIsCorrect()
{
var factorial=new Factorial();
var result =factorial.Compute(5);
Assert.ArEqual(result,120);
}
}
- 代码可扩展性:基于接口的设计、插件架构、状态机。
- 不要试图通过清洗、移动的注释来表达一段但以理解的代码。
- 可读性(3C规则):注释(Comment)、一致性(Consistency)、清晰性(Clarity)
- 代码质量衡量:可测试性、可扩展性、可读性。
4.发现领域架构
- 领域驱动设计:应对软件核心复杂性。
- 大多数情况下,三段架构(表现层、业务层、数据层)的数据模型时数据存储的关系型数据模型。
- 表现层:负责提供用来完成任务的用户界面。
- 应用程序层:
- 系统后端的入口点(一个按钮都触发系统后端里德操作,启用一个工作流程);
- 编排业务逻辑,应用程序层引用领域层和基础设施层。最后,应用程序层对业务规则一无所知,不会包含任何与业务有关的状态信息。
- 领域层:包含了所有并非针对一个或多个用力的业务逻辑。
- 领域模型:领域模型的终极目标是实现统一语言和表达业务流程所需的操作。
- 领域服务:出于某种原因无法放入热河现有实体的领域逻辑。它是一个类,包含了一些逻辑上有关系并且操作多个领域实体的行为。通常也需要访问基础设施层执行刻度写操作。
6.基础设施层:与使用具体技术有关的任何东西,不管是数据持久化、特定的安全API、日志记录、跟踪、缓存等。
##6.变现层##
###设计不只是外在,还有内在###
- 用户体验优先
- 关注交互
- 基于任务的设计(采用面向任务UI,与读写模式分离齐头并进);
- 目标是找到最佳的做事方式:关注交互而不是数据,持续去改善草图(用户界面通常更多地受后台尸体而不是想要的体验和客户端上实际执行的任务影响)
- 为每个屏幕创建一个视图模型类(一个屏幕就是一组输入),可以轻易抽象为一个类。
- 用户体验不是用户界面
- 要点:信息架构,交互设计,可视化设计,可用性审查。
- 一些工具:Axure、Balsamiq、UXPin、Wirify
- 交互:用户体验分析师现代表现层的核心,那么可用性审查就是用户体验分析的核心。
- 有效地体验
- 把交互变成视图。
- 把视图变成原型。
- 告诉客户它只是一个原型。
- 关注交互
- 真实场景:
- 一般而言,表现层有两个主要组件组成:用户界面和表现逻辑(UI逻辑)。
- 用户在用户界面里做出的任何操作都会成为表现逻辑的输入。
##7.神秘的业务层##
- 用来组织业务逻辑模式
- 事务脚本模式(TS):鼓励你跳过任何面向对象设计。把业务组件直接映射到所需的用户操作上。它推动从人物的角度看业务逻辑(提高用户体验的关键)。
- 适合业务逻辑简单,最好是不大可能改变和发展的场景。
- TS模式并未强制使用任何类型或格式进行数据交换。
- 领域模型模式(DM):忠实反映业务领域,尤其是领域里的流程和数据流。
- 在较大的系统里采用领域模式,因为它的启动和维护成本更容易被消化。
- 领域模型是一组普通类,每个类忠实地表示了业务领域里德一个重要实体。
- 反领域模式(ADM):和领域模式类似,但实体里没有行为,只有属性。
- 不必把任何逻辑放在领域对象里。所有需要的逻辑都放在一组服务组件里,这些组件共同构成完整的领域逻辑。
- ADM是反模型,尤其是在复杂的领域里使用,并且面临大量频繁更改业务的规则。就数据驱动应用程序和CRUD系统而言,评学模型已经足够了。
- 事务脚本模式(TS):鼓励你跳过任何面向对象设计。把业务组件直接映射到所需的用户操作上。它推动从人物的角度看业务逻辑(提高用户体验的关键)。
- 把焦点从数据移到任务
- ASP.NET MVC 里的任务编排:任何用户界面都会转化成控制器的类上调用的方法。避免把所有编排逻辑放入控制器方法。
- 把控制器看做协调者:责任驱动设计(RDD)的本质是把系统特性分解成系统必须执行的多个操作。接着,每个操作对应正在设计中的一个组件(通常是一个类)。执行这个操作就变成这个组件的专门责任了。这个组件的角色取决于它所承担的责任。
- 连接应用程层与表现层:应用程序层和表现层之间的交接点是控制器。在ASP.NET MVC里使用功能能齐全的控制反转(IoC)模式,需要重写控制器的工厂。
- 连接应用程序层与数据访问层:通过依赖注入。仓储是数据访问逻辑的容器的常用名称。确保仓储类型和接口(与领域模型里德重要实体一一对应)关联到IoC
- 使用依赖注入连接各层是推荐解决方案,但不是唯一的。如果逻辑层部署到不同的物理层,使用HTTP接口则是常见做法。
- 在领域里编排任务:领域服务包含任何无法放入领域实体的逻辑。
- 跨实体的领域逻辑:通常包含的业务逻辑的操作牵涉多个领域实体(服务本身的取名应该反映显示操作,并且容易被利益相关者和领域专家理解)。定义成领域服务的操作大多数情况下都是无状态的(传入某些数据,然后获得默写结果)。
- 连接字符串在哪:领域模型里的实体应该是普通C#对象,并且与持久化无关。
例子:Invoice类只包含日期、编号、客户、支付条款等数据,此外,还可能包含GetEstimateDataOfPayment方法,这个方法读取日期和支付条款,计算一下节假日,然后确定发货单的发货日期
从存储读取Invoice实体的代码和把它保存到存储的代码并没有和实体本身放在一起。系统的其他组件会处理这点。这些类就是仓储。仓储式整个系统里唯一处理连接字符串的地方
- 跨越边界传输数据:物理层意味着需要跨越的物理边界,不管是进程边界还是机器边界。跨越边界是一个昂贵的操作。
- 分层架构里的数据流:途中展示了一个相对抽象的数据流。
- 共享领域模型实体:在遵循领域模型模式的分层架构里,领域实体是罪合适的输入容器。
- 在各层的领域实体:认为应用程序层编排的组件和模块之中传递领域模型的类是没有问题的
- 为命令和查询使用单一模型的风险:这个行为本质上就是领域逻辑,而领域逻辑必须一致与业务规则保持一致,表现层代码有打破这个一致性的潜在风险。
- 将来扩展的可能约束:不想为了满足特定前端的需要而修改领域模型。于是,添加转梦的数据传输对象就变成最合适的做法了。
- 使用数据传输对象:在给定系统及其生命周期里使用单一解决方案跨越边界传输数据是很少的。
- 数据传输对象概论:数据传输对象专门用来在不同的物理层之间携带数据。
- DTO与领域实体:DTO可以把复杂的层次结构简化成简单的数据容器,只包含必要的数据。
- AutoMapper和适配器:在源类型和目标类型之间创建一个映射;启动映射过程,用源类型的实例里的数据填充目标类型的实例。
- ASP.NET MVC 里的任务编排:任何用户界面都会转化成控制器的类上调用的方法。避免把所有编排逻辑放入控制器方法。
一个成功的业务层需要敏锐的观察和建模。
5.领域模型导论
设计的模型和理念相互影响 ——Eric Evans
-
从数据到行为的转变
典型的开发方案:收集需求,通过一些分析找出相关实体和需要实现的流程。接着,带着这些理解,尝试推到能够支撑流程的无理数据模型(通常是关系型)。确保数据模型符合关系型的一致性,然后根据标识相关业务实体的表构建软件组件。可以通过存储过程等数据库特有的功能实现行为,使数据库对上层代码隐藏起来。最后一步是找到合适的模型表示数据,并把它传到表现层上。- 模型和领域背后的基本原理
- 并非关于使用对象代替数据读取器。
- 持久化对象模型并非领域模型
- 模型是什么,为什么需要模型(参考莫卡托投影)
- 关于行为的一切:领域模型的目标是尽可能地表达应用程序要处理的核心业务概念;若要与业务保持一致,你的设计应该以行为而不是数据为重点。
- DDD对每个人来说都是好的
- 数据库是基础设施
DDD的建议是关注业务概念和绑定上下文流程,并了解它们,然后规划一个可以忠实实现那些业务概念和流程的系统。产生的系统以模型为中心,包含了核心业务逻辑。DDD只是把领域建模放在前面,把它的持久化放在后面。- 领域模型无需关心持久化:需要去关心相关实体与它们的关系、事件和可观察行为。模型通过类和方法表示实体。持久化透明通常用来表达领域模型的一个关键特征,其意味着领域模型的类不应该包含从磁盘保存或构件实例的方法。类似:领域模型的类也不应该暴露需要访问持久层才能做出判断的方法。
- 应用程序应该关心持久化:所有应用程序都需要提供持久化的基础设施以及各种横切关注点(安全,缓存,日志)。最终,领域模型应该与持久化实现细节保持独立,但通常情况下,O/RM技术可能会给模型带来一些约束。
- 模型和领域背后的基本原理
-
领域层的内部
最常见的绑定上下文支撑架构师带有领域模型的分层架构。在表现层之下,分层架构通过编排代码(应用程序层)对领域和基础设施层进行操作。领域层包含模型和服务。- 领域模型:提供业务领域的概念视图。由实体和值对象构成,对现实世界进行建模,目的是要把这些概念转变成软件组件。
- 模块:当你把一个领域模型变成软件时,你会标识一个或多个模块。,模块包含对象并对整个领域进行分区,以便领域模型设计的所有关注点都能清楚、干净地分离开来。在绑定上下文里,领域模型通过模块组织。,有如下包含关系
- 一个绑定上下文有一个领域模型
- 一个绑定上下文可以有多个模块
- 一个领域模型可以关联多个模块
- 值对象:DDD包含实体和值对象。虽然二者都表示成.NET的类,但实体和值类型分别表示不同的概念,从而导致不同的实现细节。在DDD里,值对象完全通过它的特性来定义,但其特性在实例创建之后就不会再变了。如果要变,值对象会变成另一个值对象的特例,包含一组全新的特性。在.NET里,DDD值对象表示成不可变类型。
- 实体
- 所有对象都有特性,但不是所有对象都能完全通过它们的特性集合来标识。
- 值对象只是聚合在一起的数据
- 实体通常由数据和行为构成
- 领域逻辑在领域层里(模型或服务)
- 用例的实现则在应用程序层里
- 实体的持久化
领域模型必须持久化,但是,它不直接关心自己的持久化
- 模块:当你把一个领域模型变成软件时,你会标识一个或多个模块。,模块包含对象并对整个领域进行分区,以便领域模型设计的所有关注点都能清楚、干净地分离开来。在绑定上下文里,领域模型通过模块组织。,有如下包含关系
- 聚合
随着构建领域模型,单个实体总是互相引用的情况很多,这意味着“大泥团”的征兆:逻辑上相关的对象被单独对待而不是组合起来并当做一个整体对待。
聚合基本上是一致性的边界,对模型里的实体进行分组和隔离。一致性通常是事务性的,但在一些情况下也可能采取最终一致性。
通常的做法是先把领域模型分解成聚合,然后再聚合里标识出领域实体- 构思聚合模型:在领域里,单个容器下的多个实体合起来叫做聚合。最终的模型是聚合、单个实体和它们的值对象的结合
模型里的实体会被划分为聚合。每个聚合都受限于一个逻辑边界,这个边界定义了哪些实体在这个聚合里。**一个聚合有一个根实体,被称为聚合根。**聚合根是对象图的根,它封装了包含于其中的实体,并充当它们的代理。 - 使用业务不变条件发现聚合:聚合是设计的一部分。在绑定上下文里,聚合表示业务模型里的不便条件,从另一个角度来说,聚合是一种保证业务一致性的方式。
一般来说,一致性有两种:
- 事务一致性:聚合保证在领域里每次发生业务事务后的一致性。
- 最终一致性:一致性在某种程度上会得到保证,但可能不是在每次业务事务之后
聚合并不关心边界之外发生的一致性和操作,在一个良好的领域模型里,每个事务智慧修改一个聚合
-
聚合模型的好处
-
整个领域模型的实现以及它的业务逻辑变得更加简单
- 聚合模型是单纯的逻辑分组。
- 就代码而言,实体类任然是单独的类,并且每个都有自己的实现。
- 聚合模型并不一定引入新的类来组织内嵌类的代码;但是,出于方便,聚合通常写成新的类,封装子对象的整个对象图。
聚合模型提升了抽象层次并在一个整体里封装了多个实体。模型包含层次减少,因而可操作的内部实体关系的数量也减少
-
防止紧耦合模型:每个实体都可能与很多其他实体关联在一起。一个实体要么是一个聚合根,要么只被包含在一个聚合里。
-
-
聚合之间的关系
实现聚合模型只需对设计的实体类做出绩效的改变,但会对领域模型的其余部分造成很大的影响,尤其是服务和仓储。聚合的根类对调用方隐藏了相关类,并要求调用方在进行任何交互时引用它们。 -
创建聚合根
- 聚合根对象是组成这个聚合的对象群的根。
- 聚合根在整个领域模型都可见,而且可以直接引用。
- 聚合里的实体任有它们的身份标识和生命周期,但它们不能从聚合之外直接引用
聚合根对象有相应的责任:
- 聚合根保证所包含的对象总是按照应用程序业务规则出于有效状态(一致性)。
- 聚合根负责持久化所有被封装的对象。
- 聚合根负责级联更新和删除聚合里的实体。
- 查询操作只能获取聚合根。访问内部对象必须总是通过整合根的接口来完成。
提示:
- 实体在关系型转换模型里会有单独的表。
- 聚合是通过外间关系相互关联的表的联合。
- 聚合根是在外键关系图里唯一一个只包含对外外键引用的表。
- 构思聚合模型:在领域里,单个容器下的多个实体合起来叫做聚合。最终的模型是聚合、单个实体和它们的值对象的结合
- 领域服务
领域服务类的方法实现的领域逻辑不属于特定聚合,并且很可能跨越多个实体。为了实现业务操作,领域服务协调聚合和仓储的活动。在某些情况下,领域服务可能使用基础设施的服务。
当一块业务逻辑无法融入任何现有聚合,而聚合又无法通过重新设计适应操作时,就要需要考虑使用领域服务。因此,在某种程度上,领域服务收容无法放在其他地方的逻辑。领域服务使用的名字严格遵循统一语言。- 服务及契约:领域服务的接口代表统一语言里的契约,列出可供应用程序服务调用的操作。
- 跨聚合行为:领域服务的最常见场景是实现设计多个聚合或数据库访问方法的行为。模型的结构对领域服务的数量和接口有很大的影响。
- 仓储:仓储式领域读物的最常见类型,它们负责持久化聚合。每个聚合根都有一个对应的仓储。一个常见的做法是从这些类提取一个接口,并在核心模型所在的类库里定义这个接口(仓储接口的实现属于基础设施层),其接口通常定义在领域层代码所在的程序集里。一个好的仓储也可以只是一个基于接口的类,有一组根据你需要安排的成员。仓储类的成员会执行数据访问——查询、更新或者插入。
- 是服务还是聚合:倾向于认为领域服务是哪些无法自然放入聚合的行为的后背藏身之所;使用新的持久化实体而不是领域服务编排。两个方案最终都行得通。
- 领域事件
- 从顺序逻辑到事件
需求说明了当某个事件触发时要做什么。如果在领域服务方法里放置处理代码,就不得不在需求改变时触碰这个领域服务方法,否则就要为相同的事件执行多个操作。修改领域服务类本身并不危险,但一般来说,任何做法只要降低修改现有的类带来的风险都会受到欢迎。 - 在相关的事件发生时触发事件:事件使我们没有必要在一个地方放置所有处理代码。
- 使我们可以再不触碰产生事件的代码的情况下动态定义一组处理器
- 使我们可以再多个地方触发相同的事件。这意味着处理器的执行无需关心实际的调用方。
- 规范化领域事件:领域事件是一个简单的类,表示领域里引起关注的事件。领域事件可以只是定义在类上的一个成员。但是,在领域模型场景里,事件通常表示成带有专门接口标记的领域特定类。
- 处理领域事件:为领域事件定义处理器。任何注册的处理器都有机会处理特定类型的事件。处理器是一个小类,它包含一些逻辑在要对某个事件做出反应时运行。可以有多个类处理相同的领域事件,这允许组合多个操作。
- 从顺序逻辑到事件
- 横切关注点:在分层架构里,你会发现几处设施层是一个与具体技术有关的地方。
- 在单个事务的保护下:并非做了领域建模,就不必亲自处理数据库连接字符串以及级联规则、外键和存储过程这类繁琐的东西。CRUD接口的多数高级方面都由O/RM完全处理。当使用Entity Framework 时,你以事务的方式操作工作单元,框架的上下文对象会为你透明管理。工作单元的跨度只有一个数据库连接。对于多连接事务,可能要在应用程序层用这个类把一个逻辑操作的多个步骤拼在一起。最终,把数据库访问代码隔离在存储仓库,在应用程序层或领域服务里使用仓储。
- 验证:有两种主流观点
- 实体必须总是处于有效状态
- 实体应该当做普通.NET对象,直到要以某种方式使用它们为止。
因为DDD是关于忠实地建模领域,所以它应该总是处理有效地实体。
领域模型里的类应该检查它们能够检查的东西,并在出错的时候。
- 安全
- 与应用程序所在的环境有关,触及访问控制权限、跨站脚本和用户验证等。这和领域层无关。
- 与授权有关,确保只有授权用户才允许执行某些操作。
- 日志记录:决定在哪里记录什么。
- 缓存:缓存的理想实现和相关代码的位置完全取决于你真正想缓存的是什么。把用到的组件隐藏在契约背后。
出于测试和扩展的原因,可以把缓存抽象为一个API接口,然后再仓储或领域服务里注入一个有效地实现。
- 领域模型:提供业务领域的概念视图。由实体和值对象构成,对现实世界进行建模,目的是要把这些概念转变成软件组件。
-
总结:领域模型尝试根据实体和对象以及它们的关系、状态和行为对业务领域进行建模。在领域模型里,实体是业务空间里的相关对象,它有标识和生命周期。而值对象则是业务空间里死气沉沉的东西。为了编码和设计,有些实体也会组合成聚合。
#领域模型导论#
分析部分发现顶层架构,通过统一语言挖掘绑定上下文和它们之间的关系。
策略部分为每个绑定上下文提供最合适的架构。
- 分离命令与查询
查询不以任何方式修改系统状态,只返回数据。
命令则修改系统状态,但不返回数据(除了状态代码或信息确认)- CQRS:查询和命令式不同的东西,应该区别对待。
- 从领域模型到CQRS
在某种程度上,CQRS是对构思复杂领域模型的困难的一种横向思考。
CQRS使用两个不同的领域层而不是一个。这种分离把查询操作放在一层,把命令操作放在另一层。接着每一层都有自己的架构和专门服务。分别用于查询和命令。如图区别:
在CQRS里,查询栈基于SQL查询,完全没有模型、应用程序层和领域层。一般而言查询栈应该尽量简单。此外,通常的CQRS方案会为每个部分准备一个不同的数据库。 - 查询和命令领域层的结构
在CQRS系统里,查询部分的领域层的模型可以只是一组量身定做的数据传输对象(DTO)。
一般而言,很可能需要命令部分的领域模型,因为会在这里表达业务逻辑和实现业务规则。
CQRS系统的命令布冯的领域模型可能非常简单,因为它是为命令量身定制的。
领域模型是应对软件核心复杂性的理想方式。大多数复杂性来源于查询和命令的笛卡尔积。分离命令和查询可以把复杂性降低一个级别。 - CQRS不是顶层架构:不是设计企业级系统的全面方案。CQRS只是一个模式,指导你家狗更大系统的特定绑定上下文。执行基于统一语言的DDD分析,标识出绑定上下文仍是值得推荐的初期步骤。
- 从领域模型到CQRS
- CQRS的好处
- 简化设计
从使用模型中得到的教训是,你在软件系统里面临大多数复杂性通常都与改变系统状态的操作有关。命令应当验证当前状态,然后决定是否可以运行。接着,命令应该确保系统处于一致状态。 - 增强可伸缩性的潜能
可伸缩性的实施方案对所考虑的每个系统来说似乎都是唯一的。- 可伸缩性定义了系统在用户数量增加时维护相同级别性能的能力。
- 可伸缩性取决于架构师微调系统使之在相同的时间单元里执行更多操作的余地。
分离 查询和命令让你可以完全隔离处理两个部分的可伸缩性。
- CQRS的积极副作用
- 引导你深入理解你的应用查询读取什么以及处理什么。模块的严格分离也使你可以安全地改变某一个而不会对另一个造成某种回归。
- 对查询和命令的考虑引导你按照任务和机遇任务的用户界面进行分析。
- 简化设计
- 在业务层里使用CQRS
- 一个包含读取操作所需的模型和服务
- 包含命令操作所需的模型和服务
模型的形式最终只是实现细节,不管是对象模型、一个函数库、还是一组数据传输对象。
- 非协作系统与协作系统
CQRS是在寻找更有效的方式应对复杂系统时发现的。
在协作系统里,底层数据在任何时候都可能受到当前用户、通过各个前端连接的并发用户和后端软件的影响而发生改变。在协作系统里,用户竞争相同资源,意味着数据的实时性。导致这种持续改变的其中一个原因是业务逻辑特别复杂,牵涉到多个可能需要动态加载的模块。架构师有两个选择:- 在需要完成任何操作时锁定整个聚合(吞吐量很低)
- 保持聚合对改变开放,代价可能是显示过期数据,但最终会达到一致。
- CQRS
CQRS不只是使用不同的领域层来执行查询和命令,更多是使用根据下图这些新的指导原则进行架构的不同栈来执行查询和命令。
在命令通道里,然和来自表现层的请求都会变成一个命令,并加入到处理器队列。命令处理器只对命令处理一次。处理命令可能会产生事件,这些事件会被其它注册组件处理。
当业务逻辑非常复杂时,你会负担不起同步处理命令。原因如下:- 它使系统变慢
- 有关的领域服务会变得过于复杂,甚至费解,容易出现回归,尤其在规则频繁改变时。
查询通道非常简单。它就是一组仓储,从专门的去规范化数据缓存查询内容。查询通道时分开的,任何时候都可以把它放到一个专用的服务器。
- CQRS总能胜任架构需要
查询栈和命令栈之间的简单分离也会简化设计。- 在命令栈里使用事务脚本
- EDMX用作只读模型
- 务实的架构师角度
- CQRS:查询和命令式不同的东西,应该区别对待。
- 查询栈:处理过期数据的必要性
- 只读领域模型:只处理查询的模型比同事处理查询和命令的模型更简单。
- 为何需要不同的模型:领域模型是业务领域的API。一旦公开暴露,就可以调用API执行任何运行的操作。为了确保一致性,API不应该以来开发者仅以正确的方式使用。领域模型的类会改变得越来越复杂,因为它们可以同时在查询和命令场景里交替使用。
- 从领域模型到只读模型:当你的目标只是为只读操作创建领域模型时,一切都会变得更容易,类从整体上来说也会变得更简单。累的总体结构更像数据传输对象,属性也比方法变得多得多。
- 设计只读模型外观
查询栈可能仍然需要领域服务从存储提取数据,为上面的应用程序层和表现层所用。在这种情况下,领域服务,特别是仓储,应该重新界定为只允许读取存储。- 限制数据库上下文
在只读栈里,并不需要拥有全部CRUD方法的经典仓储。
实现查询通道的基本步骤是把对数据库德访问限制为只能查询。
聚合的概念在只读模型里不再重要。但是,只读模型外观里的可查询数据通常与完整领域模型的聚合对应。 - 调整仓储
就仓储而言,底线是你在查询栈里并不需要它们。整个数据访问层都能直接在应用程序层里通过在某些对象/关系模型类之上的LINQ查询来表达清楚。
- 限制数据库上下文
- 分层表达式树:当一个通用解决方案随着时间变得极其复杂,并越来越不可管理时,很可能是因为它没有很好地解决问题。在这里推荐的不同方案可以利用LINQ和表达式树的威力降低只读模型里的仓储和DTO的复杂性。
- 只读领域模型:只处理查询的模型比同事处理查询和命令的模型更简单。
- 命令栈
在CQRS里,命令栈只考虑改变应用程序状态的任务的执行。通常,应用程序层从表现层获取请求,然后编排它们的执行。-
回到表现层:命令式通过后端执行的操作。从CQRS的角度来看,任务是单向的,它会产生一个工作流,从表现层下达到领域层,最终可能修改某些存储。任务通过两种方式触发:
- 用户通过操作某些UI元素显示启动任务。
- 某些自主服务于系统异步交互。
提交的请求会更新系统的状态,但调用方法可能仍需获取某些反馈。
-
规范化命令和事件
所有软件系统都从某个前端数据源获取输入。输入数据从前端传到应用程序层,输入数据的处理阶段会在这里编排。总之,用于输入处理的任何前端请求都可以看做发送给应用程序层(接受者)的消息。消息是一个包含任何后续处理所需的普通数据的数据传输对象。前端可以通过多种方式把信息传给应用程序层。一般情况下,传输是一个普通的方法调用。
消息有两种:命令和事件。两种消息都包含了一组数据。
命令是一种命令式的消息,就像为了执行某些任务显示提交给系统的请求。- 一个命令由一个处理器管理。
- 命令可以被系统拒绝。
- 命令可以再某个处理器的执行过程中失败。
- 命令的实际效果会因系统的当前状态而有所不同。
- 命令通常不会非法入侵某个绑定上下文边界,
- 命令的推荐命名规范认为它们应该是命令式的,指出需要完成什么。
事件是一个用来通知某些事情已经发生的消息。
- 事件可以被系统拒绝或取消。
- 事件可以有多个想处理它的处理器,
- 事件的处理可以产生其他事件,由其他处理器来处理。
- 事件的订阅者可以位于发起它的绑定上下文之外。
设计领域事件的基本指导原则是它们应该尽可能具体,清楚表明目的。
-
处理命令和事件
命令是由处理器来管理的,我们通常把它称为命令总线。事件则有事件总线组件来管理。然而,命令和事件由同一个总线来处理也并非罕见。在用户界面发生任何交互都会对系统产生某些请求。- 总线组件
命令总线持有一组已知的业务流程,可以被命令触发。命令可以进一步发起这些流程的活动实例。处理命令有时候会在领域里产生事件;产生的事件会发布到相同的命令总线,或者并行的事件总线,如果有的话。处理命令和相关事件的流程通常被称为Saga
命令总线是一个类,它接受消息(执行命令的请求和事件的通知)并寻找处理它们的方式。总线本身并不实际处理事情,而是选出可以处理命令和事件的注册处理器。 - Saga组件
一般而言,Saga组件看起来像一组逻辑上相关的方法和处理器。每个Saga组件都声明了以下信息。- 一个命令或者事件启动与这个Saga关联的流程。
- 这个Saga可以处理的命令以及它感兴趣的事件。
- 命令和事件合起来的效果
当你写的系统基于非常动态的业务逻辑时,你可能会认真地考虑为无需修补系统就能扩展和改变某些工作流打开一扇门。
把工作流的整个业务逻辑放在一个方法里可能会为维护和测试带来问题。 - Saga和事务:Saga不是让单个组件知晓整个工作流,而是使用事件把整个工作流变成更小的命令处理器的组合效果,相互之间通过触发事件来协同工作。在涉及跨越多个绑定上下文的长时间运行流程时,Saga也减少了分布事务的需要。对于失败的情况,Saga方法可能会多次受到相同的事件。
- 命令总线的缺点:额外加了一层,让代码变得更不易读。
- 总线组件
-
现成的存储
大多数真实系统都会写入数据,稍后再读出来。通常,你会在命令栈里有一个领域层,在查询栈里有一个非常简单的数据访问层。- 为查询优化存储:很多真实系统使用单个数据完成读写操作。使用单个关系型数据库仍是最佳选择。在查询栈里,查询的东西与视图模型几乎有着相同的结构。
- 创建数据库缓存:为了避免赋予每个命令太多的责任,最常见的做法是任何对查询数据库有影响的命令在最后触发一个事件。然后事件处理器会负责更新查询数据库。查询数据库并不完全代表应用程序背后的业务。
- 过时的数据和最终一致性
如果在命令的最后更新查询数据库,最好自动保持命令和查询数据库同步。或者延迟命令和查询数据库之间的同步。当查询和命令数据库不同步时,表现层可能会显示过期的数据,整个系统也会保存在局部不一致性。在某个时刻,数据库会回到一致,但不是每个时刻。最终一致性通过定期运行的计划任务或异步操作的队列来实现。
-
- 总结
- CQRS提议分离领域层并且分别使用独立的模型那个进行读写。
- CQRS极其适合高协作系统,但它的简化模式也同样适用简单场景。
- CQRS的主要特点是分离命令栈和查询栈。
- 完整的CQRS是一个好的解决方案,但并不适用于所有问题。
6. 事件溯源导论#
简单是可靠的先决条件。 ——Edsger Dijkstra
推动DDD发展的动力是填补软件架构师和领域专家在业务领域上的见解差异。与关系型建模相比,DDD是一项突破,因为它促进领域建模代替数据建模。关系型建模着眼于数据实体和它们的关系。而领域建模则着眼于领域中可观察的行为。
事件溯源(Event Sourcing,ES)并非只是使用事件对业务逻辑进行建模。在ES场景里,你的数据源只包含持久化事件。
ES并非全能独立的架构,而是一个能让领域模型和CQRS等架构进一步发挥所长的特性。当把ES添加到系统时,你只是改变了数据源的结构和实现。
- 事件的突破
- 下一件大事
总的来说,认为事件是软件开发的下一件大事。绑定上下文以及领域事件和继承事件共同为架构师指明了一条道路,使它们更有效地理解和实现需求。
把可观察的业务事件看做持久化数据为开发打开了新的视角。 - 现实世界不仅有模型,还有事件
模型是现实的抽象,是架构师根据会面领域专家、利益相关者和最终用户得到的结果创建的。
最终,模型只是模型,而非我们在现实事件直接观察到的东西。 - 抛弃“最近已知的正常状态”
当我们用观察到的事件构建模型时,模型就是我们想要持久化的东西。一个模型通常是一组对象。
ES的关注点主要在你观察道德事件序列上。事件就是你想保存到持久化存储的东西。- 最近已知的正常状态:软件正常运行时,某个对象的最近已知的正常状态。
一般而言,在实体的历史里也可能有事件改编实体的结构。“最近已知的正常状态”方案是好的,但在业务领域里展开时并非有效表示项目的生命周期的理想方案。 - 跟踪刚才发生的事情
从事件溯源的角度来看,事件是系统数据的主要来源。当一个事件触发时,与之关联的数据将被保存。这样,系统就能跟踪正在发生的事情及其带来的信息。
- 最近已知的正常状态:软件正常运行时,某个对象的最近已知的正常状态。
- 事件对软件架构的深刻影响
- 你不会错过任何事情
事件的主要好处是分析报告提到的任何领域事件都能在几乎任何时候添加到系统并保存到存储。
事件并没有固定的格式或结构。事件只是一组属性,将以某种方式持久化,但不一定持久化到关系型数据库。 - 几乎无限业务场景可扩展性
事件会想你详述某个特定领域里的业务。
处理事件持久化需要新的架构元素,如事件存储,事件就是记录在这里的。 - 支持假设场景
通过储存和处理事件,可以再任何需要的时候构建内容的当前状态。
使用假设场景的可能性是使用ES的主要业务原因之一。 - 没有强制要求的技术
事件溯源并未显式地绑定任何技术或产品。
事件溯源需要某些软件工具,主要是事件存储。
事件和事件溯源关乎架构;技术按照种类界定。 - 缺点:抵制改变
如果你找到一个或多个领域专家需要你产生的事件序列,那么事件许愿就是一个进一步探索的方案。
- 你不会错过任何事情
- 下一件大事
- 事件溯源架构
决定把事件用作你的分层系统的主要数据源时你需要做什么。有两个基本方面要考虑:持久化事件以及为查询奠定基础。- 持久化事件
事件应被持久化成审计日志,并记录以发生的事情。
事件存储时普通数据库,但它不是用来持久化数据模型的。它持久化的是一组事件对象。
事件存储有三个主要特征:
①它保存的事件对象包含了重建这个时间引用的对象状态所需的任何信息。
②它必须可以返回与给定键关联的数据流。
③它只能添加存储,不支持更新和删除。
事件对象必须通过某种方式引用业务对象。- 事件和业务逻辑的整体流程
事件溯源基本上是关于以时间序列的方式捕获应用程序状态的所有改变。但是,事件是源自信息。每个事件都是事件存储里的一条记录,但每个事件都记录了已发生的某件事。 - 事件存储的选择
事件存储最终还是一个数据库。
- 事件和业务逻辑的整体流程
- 回放事件
事件溯源的主要作用是持久化消息,它使你可以跟踪应用程序状态的所有改变。- 构建业务实体的状态
- 回放事件意味着什么
- 数据快照
- 持久化事件
- 总结
事件促进基于任务的方案的分析和实现。
构建一个保罗万象的模型有时候很难,而命令和查询之间的分离展示了更加高效地构建系统的方式。
使用事件溯源时,每次重播事件都必须创建聚合状态。
#持久层#
知识只是对事实的拥有,智慧则是对它们的活用。 ——Thomas Jefferson
从应用程序的角度来看,数据源不再是物理数据库,而是构建在业务领域之上的逻辑模型
- 持久层概览
在某种程度上,现在的软件都需要访问数据,然后将数据保存或者以某种方式展示给客户。持久层这个名字通常用来指代了解数据访问那些繁杂细节的代码:字符串连接、查询语言、索引和JSON数据结构等。- 持久层的职责
持久层通常会创建成类库、被领域层(特别是领域服务)和应用程序层引用。持久层可以引用任何用于访问数据的技术。- 永久保存数据 :持久层提供一组类,它们知道如何永久保存数据。永久数据是应用程序需要处理的数据,可以再将来任何时间继续使用。
- 处理事务:持久层应该了解应用程序的事务需求,但是,持久层亲自处理的应该只有与数据访问有关的工作单元的事务。这意味着持久层应该负责在单个工作单元的某个聚合边界内更新多个表。总之,持久层的事务职责不会超出数据聚合上下文里的普通数据访问的边界。
- 读取永久数据:持久层负责从任何永久存储读取数据。出于性能的考虑,更好的办法是把读取的工作放在单独的服务器列表上,并充分利用缓存数据。持久层也是数据源内容缓存策略集中实现的理想地方。
- 仓储模式的设计
仓储是一个类,里面的每个方法都表示一个针对数据源的操作——无论是什么操作。这就是说,仓储类的实际结构在不同的场景和应用程序里可能会有较大差别。-
仓储模式:仓储式一个协调领域模型和数据映射层的组件,使用类似集合的接口访问领域对象。仓储在分层架构里所处的位置:
使用仓储的好处:
- 实现关注点分离
- 减少重复数据访问代码的可能
- 把数据访问代码用作可注入组件,增加应用程序层和领域层里的可测试代码。此外,一组良好隔离的仓储类能为一些应用程序的部署打下坚实的基础,以便一些数据访问层可以应对不同的数据库。
-
工作单元模式:Uow被定义构成一个业务事务的一组操作。一个支持这个模式的组件,比如仓储,可以协调在单个物理事务里的写入更改,其中包括解决并发问题。总而言之,支持工作单元一位置调用方可以通过把仓储暴露的操作组合起来安排逻辑事务。
-
仓储模式与CQRS:若选择CQRS,通常只在命令栈里建仓储,每个聚合对应一个仓储类。仓储类只有写入方法和一个根据ID返回聚合的Get方法。在CQRS场景里,只读栈通常不需要仓储类。
-
仓储模式与领域模型:在领域模型场景里,每个聚合都有一个仓储。这个仓储类会处理查询和命令。
-
命令仓储的接口:无论使用领域模型还是CQRS,都需要一个仓储为聚合执行写入操作。仓储基于泛型接口,这个接口通常定义在领域层。接口的实现则通常放在基础设施层的一个单独的程序集里。
-
- 持久层的职责
- 为何考虑非关系型存储
NoSQL,不仅仅是SQL。- 哪些地方正在使用NoSQL:
- 海量数据以及数百万潜在用户。
- 每秒上千次查询。
- 非结构化/半结构化数据以不同形式出现,但仍需统一处理(多态数据)。
- 使用云计算和虚拟化硬件满足极端伸缩性需要
- 数据库是一个天然的事件源
- NoSQL类型:
- 文档/对象存储
- 图存储
- 键值存储
- 表格存储
- 优缺点
-
关系型数据库
优点:- 支持标准数据访问语言(SQL)
- 表格模型易于理解,设计和规范流程定义明确。
缺点:
- 对通过SQL读写复杂类型仅提供有限支持
- 需要数据库结构的知识才能创建即席查询。
- 对大量记录进行索引(数百万行级别)会变得很慢。
-
NoSQL
优点:- 简单的扩展
- 快速读写
- 低廉的成本
缺点: - 不提供对SQL的支持
- 支持的特性不够丰富
- 现有产品不够成熟
-
- 哪些地方正在使用NoSQL:
- 总结
- 无需读写数据的应用程序和绑定上下文是不存在的。
- 读写不一定发生在相同的数据存储。其次,数据存储不一定是数据库。
- 了解系统的机制和数据的特征,然后制定出最可行的架构。
- 针对企业场景,应该使用不同的存储技术来存储不同类型的数据。
- 没有理由通过单个技术或产品来统一存储。
- 多元化持久化要求你学习不同的存储技术和产品。
俏皮话
- 今天的编程是软件工程师和老天之间的竞赛,前者努力构建更大、更好、白痴也能用的程序,后者尝试创造更大、更好的白痴。到目前为止,老天领先。
- 程序员的麻烦是你要等到一切都晚了才能搞清楚程序员在做什么。
- 给人程序,毁人一天;教人编程,毁人一生。
- 你曾遇到的最大灾难是你的第一门编程语言。
- 程序是写给人读的,写给机器运行指示偶然。
- 面向对象的致富途径是什么?当然是继承。
- 如有疑问,蛮力为之。
- 如果蛮力无法解决你的问题,那是你没用力。
- 好的判断源自经验,而经验源自坏的判断。
- 电脑就像比基尼,避免人们过多猜测。
- 如果你只知道SQL,那么所有数据看起来都是关系型的。
- 如果答案就是太多记录,你可以改写这句查询吗?
- 如果开发者可以再你设计的数据库里放入错误的数据,那么最终会有某个开发者这样做。
- 如果开发者可以把API用错,他最终会用错。
- 真实数据很少反映你对它先入为主的看法。而你通常会在最不合适的时候发现这点。
- 实践经验最不足的人通常在如何让出事方面见解最多。
- 问题的解决方案改变了问题。
- 当心虚假知识,它比无知更危险。
- 要理解递归,必须先理解递归。
- 软件在复用之前要先可用。
- 90%的东西都是CRUD。
- 如果调试是除掉软件缺陷的过程,那么编程就把它们放进去的过程。
- C很容易搬石头砸自己的脚;C++比较难做到,不过当你这样做时,它会让你半身不遂。
- 一个可工作的程序只包含未被发现的缺陷。
- 无论你有多少资源,永远都不够。
- 所有常量都是变量。
- 变量不会改变。
- 向已经延迟的软件项目增加人手会使之更加延迟。
- 没有东西可以按时、按预算完成构建。
- 失败不是一个选择,而是包含在软件里面。
- 一个项目怎样才能拖上一年?每次托一天。
- 好的判断源自经验,而经验源自坏的判断。
- 专家就是最后一刻进来骂人的人。
- 看起来好得难以置信的东西很可能是假的。
- 发现缺陷的概率与观察人的数量和重要程度成正比。
- 随着系统的演进,它的复杂度会不断增加,除非着手维护并降低复杂度。
- 如果它没经过测试,它就是坏的。
- 写代码的时候应该去假设最终维护你所写的东西的人是一个有暴力倾向的精神变态者,并且知道你住哪。
- 真正的程序员不会为他们的代码写注释。如果代码很难写,它应该也很难懂。
- 一个程序员90%的错误都来自其他程序呀的数据。
- 软件缺陷不可能被任何人发现,除了最终用户。
- 本质上,所有模型都是有错的,但有些事有用的。
- 软件总体来说就是一个“有本事来抓我”的游戏。
- 正确的实施并非及其空难;但错误地实施却极其简单。
- 由于全球变暖,需求不会冻结了,会一直流淌。就如同那句话一样:你今天见到的需求还是昨天看到的那个吗?
- 所有归纳都是无效的,包括这个。
- 最弱的环节永远是最稳定的环节。
- 永不低估可以工作的软件的价值。
- 比一瞧不同的用户更糟糕的是自以为是的用户。
- 当你设计一个程序区处理所有可能的愚蠢的错误时,大自然会创建更愚蠢的用户。
- 构建一个即使傻瓜也能使用的系统,而且只有傻瓜才愿意使用它。
- 任何傻瓜都可以写出计算机能懂的代码,但好的程序员可以写出人类能懂的代码。
- 不要因为只是个CRUD就有权利写出垃圾。
- 一个可以工作的复杂系统总是从一个可以工作的简单系统进化而来的。
- 在软件可靠性上的投入会持续增加,直到超出错误的可能代价。
- 理论和时间在理论上没有,在实践上有