系統(tǒng)設(shè)計(jì)中的命令和事件

最近和一個(gè)同事在討論基于事件的系統(tǒng)設(shè)計(jì),他認(rèn)為命令和事件是一個(gè)系統(tǒng)消息的兩個(gè)名字势腮,都是脫胎于觀察者模式她混,沒有什么不同烈钞。

其實(shí),在不久之前坤按,我也覺得這兩者在系統(tǒng)中扮演的角色沒什么不一樣毯欣,都是觸發(fā)系統(tǒng)產(chǎn)生響應(yīng)的載體。

難道這兩者真的只是一個(gè)事物的兩個(gè)名字嗎晋涣?顯然不是的。

在軟件上沉桌,有一種事件溯源(EventSourcing)的架構(gòu)模式谢鹊,其思想很簡單,就是系統(tǒng)現(xiàn)在的狀態(tài)都是由一個(gè)個(gè)事件演化而來留凭。例如

// x代表我們當(dāng)前的狀態(tài)
let x=1+2+3+4

// add 方法模擬我們的系統(tǒng)操作
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 的狀態(tài)由 初始狀態(tài) 1佃扼,經(jīng)過了 (+ 2) (+ 3)(+ 4) 事件演化成了現(xiàn)在的狀態(tài)10,這就是一個(gè)事件溯源的思想蔼夜,描述了系統(tǒng)狀態(tài)一步步怎么演化過來的兼耀,在很系統(tǒng)中,需要不僅記錄單據(jù)當(dāng)前的狀態(tài)求冷,也需要記錄單據(jù)變更日志瘤运,如果我們以事件溯源的方法去構(gòu)建系統(tǒng),尤其是對(duì)數(shù)據(jù)安全性要求很高的系統(tǒng)匠题,我們天然的有兩個(gè)記錄對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)了拯坟。我們記錄當(dāng)前狀態(tài)的那一行數(shù)據(jù)記錄和事件記錄狀態(tài)發(fā)生不匹配的時(shí)候,很容易找到系統(tǒng)的bug韭山。最簡單的方式 郁季,就是把事件重放一遍,狀態(tài)就恢復(fù)成正常的了钱磅。

是不是覺得這種方案很美好梦裂?

但是這個(gè)方案目前為止有個(gè)缺點(diǎn),用代碼表示一下

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

不是我手滑盖淡,復(fù)制了兩遍年柠,只是我把代碼重復(fù)執(zhí)行了兩次,模擬事件回放的過程褪迟,y的值是沒變彪杉,但是我們的日志卻輸出了兩遍毅往,沒問題?那如果我把console.log 換成函數(shù)調(diào)用呢派近?調(diào)用了兩次其他的服務(wù)攀唯,問題就比較嚴(yán)重了!

那么如何解決這個(gè)問題呢渴丸?

在給出答案之前侯嘀,我們?cè)倏匆粋€(gè)例子:

 function buyCoffee(creditCard){
         // 調(diào)用外部系統(tǒng)支付
        charging(creditCard,1.00)
        let cup=new Coffee()
        return cup
    }

這里簡單的模擬了購買一杯咖啡的過程谱轨,客戶給了我們一張信用卡,我們先從這張信用卡上扣掉了一塊錢土童,然后做了一杯咖啡,返回給客戶献汗。這是最自然的故事節(jié)奏敢订。這個(gè)過程中,發(fā)生了兩件事罢吃,“扣款成功”楚午,“生產(chǎn)了一杯咖啡”尿招,而命令則是“買一杯咖啡”。在我們?nèi)粘>帉懘a的過程中就谜,如果有人需要監(jiān)聽這兩個(gè)事件怪蔑,

則可能是下面的程序了

 function buyCoffee(creditCard){
         //調(diào)用外部系統(tǒng)了
        charging(creditCard,1.00)
        eventBus.publish(new ChargingEvent(creditCard,1.00))
        let cup=new Coffee()
        eventBus.publist(new SaleCoffeeEvent(cup))
        return cup
    }

我們重構(gòu)一下這個(gè)程序

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()
    }

就目前來說,這個(gè)程序 沒有什么優(yōu)化余地了丧荐,看起來也比較“漂亮”了饮睬。但是到這里就結(jié)束了嗎篮奄?

如果客戶同時(shí)買兩杯咖啡怎么辦?(可以看一次買多件(種) 商品)

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

這樣處理可以嗎窟却?似乎不行吧。在現(xiàn)實(shí)生活中菩帝,去超市買東西,收銀員會(huì)跟你每件商品都結(jié)一次賬嗎呼奢?就算會(huì)多次結(jié)賬宜雀,這里用的是信用卡握础,每刷一次卡都有一筆手續(xù)費(fèi),顯然是合并收費(fèi)來的更劃算简烘。退一步講定枷,如果第n次刷卡失敗了孤澎,前面每次刷卡的錢要退回去嗎欠窒?

讓我們看看如何合適的處理這個(gè)問題

    
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)
   // 費(fèi)用
 const {creditCard,fee}=charge
  //這里調(diào)用外部
 charging(creditCard,fee)

要理解這個(gè)寫法岖妄,我們首先要明白一個(gè)概念——副作用。副作用指的是調(diào)用函數(shù)時(shí)對(duì)外部系統(tǒng)產(chǎn)生了影響衣吠,由于這種影響可以被傳播壤靶,所以函數(shù)調(diào)用者并不知道調(diào)用函數(shù)會(huì)產(chǎn)生多大的代價(jià)。我們這個(gè)需求中贮乳,信用卡扣款就是一種副作用忧换,如果不能控制這種副作用的影響范圍向拆,我們的組件是不能被組合和復(fù)用亚茬,系統(tǒng)中就會(huì)充斥著各種“過程”浓恳。

而更好的辦法就是,推遲副作用颈将。我們可以在內(nèi)存中先計(jì)算好結(jié)果,由過程控制器去對(duì)結(jié)果進(jìn)行合并后再保存起來颂砸。

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

在processor中我們調(diào)用領(lǐng)域模型進(jìn)行計(jì)算,在apply中對(duì)具體領(lǐng)域事件進(jìn)行操作人乓,比如轉(zhuǎn)換成數(shù)據(jù)庫對(duì)象,保存數(shù)據(jù)庫或者調(diào)用MQ碰缔,把領(lǐng)域事件發(fā)布出去。

而handler上面還有一層手负,是我們的Application層姑尺,就是我們的系統(tǒng)功能層了。

事實(shí)上很多軟件框架都對(duì)命令和事件進(jìn)行了區(qū)分切蟋,最常見的例子是mvvm框架vue,確切的說是vue之上的vuex喘鸟,將系統(tǒng)過程分成了兩部分MUTATIONACTION ,action純粹的修改狀態(tài),mutation負(fù)責(zé)函數(shù)調(diào)用什黑。我們上面的例子中process就是mutation, apply就是action堪夭。

命令和事件在系統(tǒng)設(shè)計(jì)中的不同大概就介紹到這里了,那么問題來了森爽,到底如何進(jìn)行安全的狀態(tài)重建呢?這個(gè)留給諸君思考吧橘蜜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末付呕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子徽职,更是在濱河造成了極大的恐慌象颖,老刑警劉巖活箕,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異克蚂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)摸恍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門赤屋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人类早,你說我怎么就攤上這事$哉伲” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵嵌巷,是天一觀的道長室抽。 經(jīng)常有香客問我,道長晓折,這世上最難降的妖魔是什么神年? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任行嗤,我火速辦了婚禮,結(jié)果婚禮上栅屏,老公的妹妹穿的比我還像新娘。我一直安慰自己护奈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布霉旗。 她就那樣靜靜地躺著,像睡著了一般读拆。 火紅的嫁衣襯著肌膚如雪鸵闪。 梳的紋絲不亂的頭發(fā)上檐晕,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天辟灰,我揣著相機(jī)與錄音篡石,去河邊找鬼芥喇。 笑死凰萨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的湿诊。 我是一名探鬼主播瘦材,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼食棕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起眶拉,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忆植,沒想到半個(gè)月后谒臼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朝刊,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜈缤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年底哥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咙鞍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡易阳,死狀恐怖吃粒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徐勃,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布肖爵,位于F島的核電站,受9級(jí)特大地震影響劝堪,放射性物質(zhì)發(fā)生泄漏揉稚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一搀玖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芳来,春花似錦猜拾、人聲如沸即舌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芜飘。三九已至磨总,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚪燕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國打工馆纳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鉴裹。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓钥弯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脆霎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 單例模式 適用場景:可能會(huì)在場景中使用到對(duì)象鹦马,但只有一個(gè)實(shí)例忆肾,加載時(shí)并不主動(dòng)創(chuàng)建,需要時(shí)才創(chuàng)建 最常見的單例模式客冈,...
    Obeing閱讀 2,076評(píng)論 1 10
  • 找到fullcalendar.js, 找到代碼為 isRTL:false遇绞,這句話 輸入以下幾句 monthName...
    迷你小小白閱讀 1,689評(píng)論 0 1
  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 2,701評(píng)論 0 3
  • 不支持上傳文件摹闽,所以就復(fù)制過來了。作者信息什么的都沒刪付鹿。對(duì)前端基本屬于一竅不通,所以沒有任何修改舵匾,反正用著沒問題就...
    全棧在路上閱讀 1,968評(píng)論 0 2
  • pyspark.sql模塊 模塊上下文 Spark SQL和DataFrames的重要類: pyspark.sql...
    mpro閱讀 9,464評(píng)論 0 13