.net架构设计读书笔记--第三章 第10节 命令职责分离(CQRS)简介(Introducing CQRS)
一、分离查询命令 Separating commands from queries
早期的面向DDD设计方法的难点是如何设计一个类,这个类要包含域的方方面面。通常来说,任务软件系统方法调用可以分为两类:查询和命令。在这里,查询是指一个系统的和个操作,它不会改变系统的任务值,仅返回一些结果。命令的职责是个性系统数据。 如果两组方法都使用相同的域模型,逻辑上可能存在查询和命令分离不明显问题,所以引入新的设计模式。
- 从域模型到 CQRS
从某种程序度上,CQRS是复杂的域模型设计的一种横向设计思维。如果域模型客观上就是复杂的,我们使用CQRS是否还需要? 从目前的设计理念看来,CQRS使用的是两个不同的域模型,而不是一个。分离是通过将查询和命令分离到两个层上,它们各有自己的架构体系和服务集合。
- 查询和命令域层结构
命令和查询的简单对系统的设计影响可能让人惊讶。系统的组织作为两个并行分支,如图上图 所示的体系结构,有一个正式的域模型是系统的严格要求。
在提供查询服务的时候我们可能不需要域模型,查询仅仅是用记接口用记查询数据的方法。查询的结果中有可能有命令,所以域模型中设计的各种关系和约束都是没必要的。域模型的查询区域专门定制简单的数据传输类DTO做为数据载体。这种情况下,域服务可能成为使用弱类来实现业务有的某些功能。
- CQRS不是顶层设计
不同 于DDD,CQRS 不是企业级系统设计的综合方法。CQRS是为你在大型系统中为上下文边界设计的一个指导模式。DDD设计基本统一语言,贯穿于整个系统设计。
- CQRS的优点
-简单化设计 Simplifcation of the design
在域模型交互设计中,通常要对面系统的复杂点是改变系统状态的操作。命令需要验证当前状态并决定是否执行,命令同步要保持系统数据的一致性。在读写共享相同的数据实体时很难确保一个操作不会做出意料之外的读写操作。早期我们就认识到模型的命令复杂度以笛卡尔积式增长的。称N为查询和命令的复杂度。在单个域模型中,在查询的规定和约束影响命令和反之亦然,如同笛卡尔积,复杂度的成长为N*N。如果将查询和命令分离,模型的复杂度则为N+N。
-增强可扩展性的潜力 Potential for enhanced scalability
可扩展性有很多方面的因素,针对性解决方法是保持每一个系统的唯一性。通常,可扩展性指系统的可维护性以及在用户增长数量级下的性能。架构 的可扩展性取决于大多数方法的操作类型,如果读取是主要的操作类型,可以引入缓存彻底解决数据库的读取压力。如果是写操作将系统拖慢,应考虑使用异步写替代同步,或使用队列。读写分离后,对系统的可扩展性可以更容易、更有针对性的处理。
- 在业务逻辑层使用CQRS
CQRS实际上没有什么负面影响,如何使用CQRS取决你如何理解CQRS,目前来说它只用于一种模式,在两个不同的层,一个是查询服务层一个是命令服务层。系统中所有的部分就会由CQRS带来益处,并且不需要太多的学习成本。
- 查询堆栈 The query stack
只读域模型 The read domain model
一个只用于读的域模型要比读写兼备的域模型简单的多。有下面一个问题,一个Order类中有一个产品类项列表属性。该属性本质上包含可枚举的数据,但不知道Items应该是哪种类型,第一种方法是使用泛型IList<T>,这种方法可以实现:
public IList<OrderItem> Items { get; private set; }
使用ReadOnly属性是更好的先择。Read-only不允许更改集合的结构,此外,如果作为包装器,用于常规的列表创建只读集合,则对基础列表的更改不影响只读包装
public class Order
{
private readonly IList<OrderItem> _items;
public Order()
{
_items = new List<MOrderItem>();
}
public ReadOnlyCollection<OrderItem> Items
{
get
{
return new ReadOnlyCollection<OrderItem>(_items);
}
}
public void Add(int id, int quantity)
{
_items.Add(new OrderItem(id, quantity));
}
}
public class OrderItem
{
public OrderItem(int id, int quantity)
{
Quantity = quantity;
ProductId = id;
}
public int Quantity { get; /*private*/ set; }
public int ProductId { get; /*private*/ set; }
}
设计只读模型
查询堆栈可能仍然需要域服务从存储中提取数据,并为它服务达应用程序和表示层。在这种情况下,域名服务和专门的存储库,应将重定向允许只读取的操作在存储上。在这种情况下,域名服务和专门的存储库应将重定向允许只读取的操作在存储上。
……………………………………
- 命令堆栈The command stack
的CQRS场景下,Command是的唯一作用就是改变系统的数据。通常应用层接受来处表现层的数据并执行。命令是针对后端,如注册一个新用户、 处理购物车的内容或更新的客户配置文件等数据落地操作。CQRS 的角度来看,命令就是数据持久化的单向操作。
任务有两种方式被触发,一种是用户在UI上明确的开始一项任务,别一种是由系统的一些服务自动触发的任务。命令的主要任务是更新系统数据,但有时候调用都需要返回一些数据来确认调用是否成功。
- 命令与事件
有两种类型的消息:命令和事件。两种类型中命令是一种数据包,命令是系统执行请求的必要数据。它们有相同点也有不同点
命令由调用者直接发出
命令可以被系统驳回
命令可能会执行失败
基于网络的命令会依赖系统的当前状态
事件不能由系统驳回或取消
事件可以有多外处理者
The processing of an event can, in turn, generate other events for other handlers to process.
An event can have subscribers located outside the bounded context from which it originated
事件类写法,如下面的代码,命令和事件都继承自Message类。
public class CheckoutCommand : Message
{
public string CartId { get; private set; }
public string CustomerId { get; private set; }
public CheckoutCommand(string cartId, string customerId)
{
CartId = cartId;
CustomerId = customerId;
}
}
Conversely, here's the layout of an event class.
public class DomainEvent : Message
{
// Common properties
...
}
public class OrderCreatedEvent : DomainEvent
{
public string OrderId { get; private set; }
public string TrackingId { get; private set; }
public string TransactionId { get; private set; }
public OrderCreatedEvent(string orderId, string trackingId, string transactionId)
{
OrderId = orderId;
TrackingId = trackingId;
TransactionId = transactionId;
}
}
命令与事件处理
命令由一个被称为Command bus的处理者来管理。事件由Event bus组件来管理。有此时候命令和事件由同一个bus来处理。下图是基于一个事件的CQRS解决方案。所有的任务都是由用户接口发起,在Asp.net MVC中Controller中的Action接收请求并向应用层发起命令。
Bus 组件
Command Bus持有一系统已知业务处理器,这些处理器可以有命令来触发。事件的处理同时会在域中产生许多事件。生成的事件被发布到同一个命令bus或Event bus。Comand Bus是一个接收消息并且找出执行方法的单独的类,Bus类不会自己执行实际要处理的任务,它会选择一个已注册的处理者来处理事件或命令。
public interface IHandles
{
void Handle(T message);
}
接口同时处理命令和事件
public class Bus
{
private static readonly Dictionary<Type, Type> SagaStarters =
new Dictionary<Type, Type>();
private static readonly Dictionary<string, object> SagaInstances =
new Dictionary<string, object>();
public static void RegisterSaga<TStartMessage, TSaga>()
{
SagaStarters.Add(typeof(TStartMessage), typeof(TSaga));
}
public static void Send<T>(T message) where T : Message
{
// Publish the event
if (message is IDomainEvent)
{
// Invoke all registered sagas and give each
// a chance to handle the event.
foreach (var saga in SagaInstances)
{
var handler = (IHandles<T>)saga;
if (handler != null)
handler.Handle(message);
}
}
// Check if the message can start one of the registered sagas
if (SagaStarters.ContainsKey(typeof(T)))
{
// Start the saga creating a new instance of the type
var typeOfSaga = SagaStarters[typeof(T)];
var instance = (IHandles<T>)Activator.CreateInstance(typeOfSaga);
instance.Handle(message);
// At this point the saga has been given an ID;
// let's persist the instance to a (memory) dictionary for later use.
var saga = (SagaBase)instance;
SagaInstances.Add(saga.Data.Id, instance);
return;
}
// The message doesn't start any saga.
// Check if the message can be delivered to an existing saga instead
if (SagaInstances.ContainsKey(message.Id))
{
var saga = (IHandles<T>)SagaInstances[message.Id];
saga.Handle(message);
// Saves saga back or remove if completed
if (saga.IsComplete())
SagaInstances.Remove(message.Id);
else
SagaInstances[message.Id] = saga;
}
}
}
Bus的功能就是做命令映射和事件分发。
Saga组件
一般情况下,一个Saga组件看起来像逻辑上相关的方法和事件处理程序的集合。每个Saga是一个组件,它声明了以下信息:
- 命令或启动与Saga组件关联进程的事件
- 命令Saga组件可以处理和aga组件感兴趣的事件
曾经年少多少事 而今皆付谈笑中!