最近和一個(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)過程分成了兩部分MUTATION 和ACTION ,action純粹的修改狀態(tài),mutation負(fù)責(zé)函數(shù)調(diào)用什黑。我們上面的例子中process就是mutation, apply就是action堪夭。
命令和事件在系統(tǒng)設(shè)計(jì)中的不同大概就介紹到這里了,那么問題來了森爽,到底如何進(jìn)行安全的狀態(tài)重建呢?這個(gè)留給諸君思考吧橘蜜。