系统设计中的命令和事件

最近和一个同事在讨论基于事件的系统设计,他认为命令和事件是一个系统消息的两个名字,都是脱胎于观察者模式,没有什么不同。

其实,在不久之前,我也觉得这两者在系统中扮演的角色没什么不一样,都是触发系统产生响应的载体。

难道这两者真的只是一个事物的两个名字吗?显然不是的。

在软件上,有一种事件溯源(EventSourcing)的架构模式,其思想很简单,就是系统现在的状态都是由一个个事件演化而来。例如

// x代表我们当前的状态
let x=1+2+3+4

// add 方法模拟我们的系统操作
function add(a,b){
  console.log("= "+ a +"+"+ b)
  return a+b
}

let y=add(add(add(1,2),3),4)
//= 1+2
//= 3+3
//= 6+4

上面的例子中 x 的状态由 初始状态 1,经过了 (+ 2) (+ 3)(+ 4) 事件演化成了现在的状态10,这就是一个事件溯源的思想,描述了系统状态一步步怎么演化过来的,在很系统中,需要不仅记录单据当前的状态,也需要记录单据变更日志,如果我们以事件溯源的方法去构建系统,尤其是对数据安全性要求很高的系统,我们天然的有两个记录对数据进行校验了。我们记录当前状态的那一行数据记录和事件记录状态发生不匹配的时候,很容易找到系统的bug。最简单的方式 ,就是把事件重放一遍,状态就恢复成正常的了。

是不是觉得这种方案很美好?

但是这个方案目前为止有个缺点,用代码表示一下

let y=add(add(add(1,2),3),4)
//= 1+2
//= 3+3
//= 6+4
 y=add(add(add(1,2),3),4)
//= 1+2
//= 3+3
//= 6+4

不是我手滑,复制了两遍,只是我把代码重复执行了两次,模拟事件回放的过程,y的值是没变,但是我们的日志却输出了两遍,没问题?那如果我把console.log 换成函数调用呢?调用了两次其他的服务,问题就比较严重了!

那么如何解决这个问题呢?

在给出答案之前,我们再看一个例子:

 function buyCoffee(creditCard){
         // 调用外部系统支付
        charging(creditCard,1.00)
        let cup=new Coffee()
        return cup
    }

这里简单的模拟了购买一杯咖啡的过程,客户给了我们一张信用卡,我们先从这张信用卡上扣掉了一块钱,然后做了一杯咖啡,返回给客户。这是最自然的故事节奏。这个过程中,发生了两件事,“扣款成功”,“生产了一杯咖啡”,而命令则是“买一杯咖啡”。在我们日常编写代码的过程中,如果有人需要监听这两个事件,

则可能是下面的程序了

 function buyCoffee(creditCard){
         //调用外部系统了
        charging(creditCard,1.00)
        eventBus.publish(new ChargingEvent(creditCard,1.00))
        let cup=new Coffee()
        eventBus.publist(new SaleCoffeeEvent(cup))
        return cup
    }

我们重构一下这个程序

function charging(creditCard,amount) {
  charging(creditCard,amount)
  eventBus.publish(new ChargingEvent(creditCard,amount))
}
function saleCoffee(){
       let cup=new Coffee()
        eventBus.publist(new SaleCoffeeEvent(cup))
        return cup
}
function buyCoffee(creditCard){
        charging(creditCard,1.00)
        return saleCoffee()
    }

就目前来说,这个程序 没有什么优化余地了,看起来也比较“漂亮”了。但是到这里就结束了吗?

如果客户同时买两杯咖啡怎么办?(可以看一次买多件(种) 商品)

function buySomeCoffee(creditCard,count){
  let array=new Array()
  for(int i=0;i<count;i++){
    array.push(buyCoffee(creditCard))
  }
  return array
}

这样处理可以吗?似乎不行吧。在现实生活中,去超市买东西,收银员会跟你每件商品都结一次账吗?就算会多次结账,这里用的是信用卡,每刷一次卡都有一笔手续费,显然是合并收费来的更划算。退一步讲,如果第n次刷卡失败了,前面每次刷卡的钱要退回去吗?

让我们看看如何合适的处理这个问题

    
function saleCoffee(){
       let cup=new Coffee()
        return new SaleCoffeeEvent(cup)
}

function charging(creditCard,amount) {
  let charge=new Charging(creditCard,amount)
 return  new ChargingEvent(charge)
}


function buyCoffee(creditCard){
    const coffee=saleCoffee()
    const fee=charging()
    const charge={creditCard,fee}
    return {coffee,charge}
} 
    
function buyCoffees(creditCard,count){
   const turples=new Array(count).fill(buyCoffee(creditCard))
   const coffees=turples.map({coffeeEvent}=>coffeeEvent.coffee)
   const charges=turples.map({chargeEvent}=>chargeEvent.charge)
   const charge=charges.reduce((a1,a2)=>{creditCard:a1.creditCard,fee:a1.fee+a2.fee})
   return {coffees,charge}
}
 const {coffees,charge}=buyCoffees('1233445',12)
   // 费用
 const {creditCard,fee}=charge
  //这里调用外部
 charging(creditCard,fee)

要理解这个写法,我们首先要明白一个概念——副作用。副作用指的是调用函数时对外部系统产生了影响,由于这种影响可以被传播,所以函数调用者并不知道调用函数会产生多大的代价。我们这个需求中,信用卡扣款就是一种副作用,如果不能控制这种副作用的影响范围,我们的组件是不能被组合和复用,系统中就会充斥着各种“过程”。

而更好的办法就是,推迟副作用。我们可以在内存中先计算好结果,由过程控制器去对结果进行合并后再保存起来。

public interface Handler{
  <T extends Command,R extends DomainEvent> List<R > process(T command);
  <T extends DomainEvent> void apply(T event)
}

在processor中我们调用领域模型进行计算,在apply中对具体领域事件进行操作,比如转换成数据库对象,保存数据库或者调用MQ,把领域事件发布出去。

而handler上面还有一层,是我们的Application层,就是我们的系统功能层了。

事实上很多软件框架都对命令和事件进行了区分,最常见的例子是mvvm框架vue,确切的说是vue之上的vuex,将系统过程分成了两部分MUTATIONACTION ,action纯粹的修改状态,mutation负责函数调用。我们上面的例子中process就是mutation, apply就是action。

命令和事件在系统设计中的不同大概就介绍到这里了,那么问题来了,到底如何进行安全的状态重建呢?这个留给诸君思考吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容