通過(guò)事件進(jìn)行應(yīng)用程序的設(shè)計(jì)是自20世紀(jì)80年代后期以來(lái)的一種實(shí)踐。我們可以在前端或后端的任何地方使用事件。當(dāng)按下按鈕時(shí),某些數(shù)據(jù)發(fā)生更改或執(zhí)行某個(gè)后端動(dòng)作。
但是事件究竟是什么呢?我們什么時(shí)候應(yīng)該用它呢?缺點(diǎn)是什么?
What/When/Why
當(dāng)類或組件之間內(nèi)聚性很高,它們的耦合度應(yīng)該很低,也就是說(shuō)當(dāng)組件需要相互協(xié)作調(diào)用時(shí),比如我們假設(shè)一個(gè)組件“A”需要觸發(fā)組件“B”中的一些邏輯,自然的方式是直接讓組件A調(diào)用組件B中的一個(gè)方法。但前提是A必須知道B的存在,這樣它們之間就是耦合的,A必須依賴于B了,這會(huì)使得系統(tǒng)更難以改變和維護(hù)。因此,這里可以使用事件來(lái)防止這種直接調(diào)用的耦合。
此外,使用事件實(shí)現(xiàn)組件解耦也有其另外的,如果我們有一個(gè)只負(fù)責(zé)組件B的工作團(tuán)隊(duì),那么他們則可能不需要與負(fù)責(zé)組件A的團(tuán)隊(duì)進(jìn)行交流,直接針對(duì)組件A中的邏輯改變?cè)诮M件B中做出相對(duì)反應(yīng)。兩個(gè)組件團(tuán)隊(duì)可以獨(dú)立發(fā)展(banq注:微服務(wù)特點(diǎn)之一), 我們的應(yīng)用系統(tǒng)變得更靈活。
即使在同一個(gè)組件團(tuán)隊(duì)中,有時(shí)候我們不需要在同一請(qǐng)求/響應(yīng)中立即執(zhí)行一個(gè)動(dòng)作的結(jié)果,只要異步執(zhí)行這個(gè)動(dòng)作,比如發(fā)送電子郵件。在這種情況下,我們可以立即向用戶返回響應(yīng),并以異步方式發(fā)送電子郵件,并避免讓用戶等待發(fā)送電子郵件。
不過(guò),如果我們不加區(qū)別地使用它,也有危險(xiǎn)。我們會(huì)遇到邏輯流程的風(fēng)險(xiǎn),這些邏輯流程在概念上是高度凝聚力的,但是通過(guò)采取脫鉤機(jī)制的事件連接在一起。換句話說(shuō),應(yīng)該在一起的代碼將被分開(kāi),并且難以跟蹤它的流程(類似于goto語(yǔ)句),不易于理解:可能是意大利面一樣混在一起!
?
為了防止將我們的代碼庫(kù)變成一大堆意大利面條,我們應(yīng)該將事件的使用限制在明確的情況下。根據(jù)我的經(jīng)驗(yàn),有三種使用事件的情況:
(1)去耦組件
(2)執(zhí)行異步任務(wù)
(3)跟蹤狀態(tài)變化(審核日志)
1.去耦組件(微服務(wù))
當(dāng)組件A執(zhí)行的邏輯需要觸發(fā)組件B的邏輯時(shí),不要直接調(diào)用它,我們可以將觸發(fā)事件發(fā)送到事件分派器。組件B將偵聽(tīng)調(diào)度程序中的特定事件,并在事件發(fā)生時(shí)執(zhí)行操作。
這意味著A和B都將取決于調(diào)度器和事件,但他們之間將不會(huì)知道對(duì)方存在,它們將被解耦。
理想情況下,調(diào)度員和事件都不應(yīng)該在兩個(gè)組件之間存在:
(1)調(diào)度員應(yīng)該是完全獨(dú)立于我們應(yīng)用程序的庫(kù),因此使用依賴管理系統(tǒng)安裝在通用位置。在PHP世界中,這是使用Composer等安裝在vendor文件夾中的東西。
(2) 事件是我們的應(yīng)用程序的一部分,應(yīng)該在兩個(gè)組件之間生存,組件之間通過(guò)事件進(jìn)行通訊(結(jié)構(gòu)上解耦,行為上耦合)。事件在組件之間共享,它是應(yīng)用程序的核心部分。事件在DDD中屬于共享內(nèi)核Shared Kernel的一部分。這樣,兩個(gè)組件都將依賴于共享內(nèi)核,但彼此不會(huì)意識(shí)到。然而在單體Monolithic應(yīng)用程序中,為方便起見(jiàn),可以將其放在觸發(fā)事件的組件中。
DDD共享內(nèi)核
[。..]明確界定指定團(tuán)隊(duì)同意分享的領(lǐng)域模型的一些子集。保持這個(gè)內(nèi)核很小。[。..]這個(gè)明確共享的東西有特殊的地位,如果沒(méi)有與其他團(tuán)隊(duì)協(xié)商,不應(yīng)該改變。
Eric Evans 2014, 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)參考
2.執(zhí)行異步任務(wù)
有時(shí)候我們有一個(gè)我們想要執(zhí)行的邏輯,但它可能需要相當(dāng)長(zhǎng)的時(shí)間來(lái)執(zhí)行,我們不想讓用戶等待它完成。在這種情況下,希望將其作為異步工作運(yùn)行,并立即返回給用戶的消息,通知他請(qǐng)求將在以后異步執(zhí)行。
例如,在網(wǎng)上商店下訂單可以同步完成,但發(fā)送通知用戶的電子郵件可以進(jìn)行異步。
在這種情況下,我們可以做的是觸發(fā)一個(gè)將被排隊(duì)的事件,直到一個(gè)工作任務(wù)可以獲得這個(gè)事件并執(zhí)行它,只要系統(tǒng)有資源。
在這些情況下,相關(guān)聯(lián)的邏輯是否在相同的有界環(huán)境中并不重要,無(wú)論哪種方式,邏輯都是去耦的。
3.跟蹤狀態(tài)變化(審計(jì)日志)
以傳統(tǒng)的數(shù)據(jù)存儲(chǔ)方式,我們擁有一些數(shù)據(jù)的實(shí)體。當(dāng)這些實(shí)體中的數(shù)據(jù)發(fā)生變化時(shí),我們只需更新數(shù)據(jù)庫(kù)表行以反映新值。
這里的問(wèn)題是,我們并不存儲(chǔ)這些值為什么改變且什么時(shí)候改變。
我們可以將這些改變的事件存儲(chǔ)在審計(jì)日志中。
更多關(guān)于這個(gè)進(jìn)一步的前景,在關(guān)于事件溯源的解釋。
事件模式
Martin Fowler確定了三種不同類型的事件模式:
(1)事件通知
(2)事件執(zhí)行狀態(tài)轉(zhuǎn)移
(3)事件溯源Event Sourcing
所有這些模式共享相同的關(guān)鍵概念:
(1)事件是代表發(fā)生了一些事情(發(fā)生在某事之后);
(2)事件被廣播到正在監(jiān)聽(tīng)的任何代碼(代碼可以對(duì)事件做出反應(yīng))。
一。 事件通知
假設(shè)我們有一個(gè)具有明確定義的組件作為應(yīng)用程序核心。理想情況下,這些組件是完全相互分離的,但是它們的一些功能需要在其他組件中執(zhí)行一些邏輯。
最典型的情況如前所述:當(dāng)組件A執(zhí)行的邏輯需要觸發(fā)組件B的邏輯時(shí),A不是直接去調(diào)用B,而是觸發(fā)事件將且發(fā)送到事件調(diào)度程序。組件B將偵聽(tīng)調(diào)度程序中的特定事件,并在事件發(fā)生時(shí)執(zhí)行操作。
重要的是,這種模式的一個(gè)特征是事件攜帶最少的數(shù)據(jù)。它只為聽(tīng)眾提供足夠的數(shù)據(jù),以便知道發(fā)生了什么并執(zhí)行其代碼,通常只是實(shí)體ID,也可能是事件創(chuàng)建的日期和時(shí)間。
優(yōu)點(diǎn)
(1)彈性更大:將事件排隊(duì)后,發(fā)送方組件可以繼續(xù)執(zhí)行其自己邏輯,即使由于錯(cuò)誤發(fā)生,因?yàn)樗鼈兣抨?duì)等候,它們可以在錯(cuò)誤被修復(fù)時(shí)被執(zhí)行。
(2)降低延遲,如果事件排隊(duì),用戶不需要等待該邏輯執(zhí)行;
團(tuán)隊(duì)可以獨(dú)立發(fā)展組件,使他們的工作更輕松,更快,更容易出現(xiàn)問(wèn)題,更靈活;
缺點(diǎn)
(1)如果沒(méi)有使用標(biāo)準(zhǔn),有可能變成一堆意大利面條代碼。
二。 事件執(zhí)行狀態(tài)轉(zhuǎn)移
讓我們?cè)俅慰纯辞懊胬?,一個(gè)具有明確定義的組件作為應(yīng)用程序核心。如果A組件一些功能需要來(lái)自其他組件的數(shù)據(jù)。獲得該數(shù)據(jù)的最自然的方法是詢問(wèn)其他組件,但這意味著被查詢組件必須提供查詢方法以供查詢組件使用,一次兩次修改增加無(wú)所謂,如果頻繁要求被查詢組件提供新的查詢方法,說(shuō)明這兩個(gè)組件彼此耦合!
在組件之間共享數(shù)據(jù)的另一種方法是:擁有數(shù)據(jù)的組件觸發(fā)的更改事件時(shí),該事件將攜帶全新更改后的數(shù)據(jù)。對(duì)該數(shù)據(jù)感興趣的組件將會(huì)監(jiān)聽(tīng)這些事件,從事件中獲得數(shù)據(jù)并存儲(chǔ)該數(shù)據(jù)的本地副本,然后進(jìn)一步對(duì)這些全新數(shù)據(jù)做出反應(yīng)。這樣,當(dāng)他們需要外部數(shù)據(jù)時(shí),他們其實(shí)在本地已經(jīng)擁有它們,它們將不需要查詢其他組件,也不需要其他組件提供對(duì)應(yīng)的查詢方法。
優(yōu)點(diǎn)
(1)更大的彈性,查詢組件不依賴被查詢組件,如果被查詢組件變得不可用(因?yàn)橛幸粋€(gè)錯(cuò)誤或遠(yuǎn)程服務(wù)器是不可達(dá)到的),查詢組件自身能正常工作,因?yàn)閾碛辛吮徊樵兘M件中主數(shù)據(jù)的本地?cái)?shù)據(jù);
(2)減少延遲,因?yàn)闆](méi)有遠(yuǎn)程呼叫(假設(shè)被查詢組件是遠(yuǎn)程的)訪問(wèn)數(shù)據(jù);
(3)我們不必?fù)?dān)心被查詢組件上的負(fù)載了,不用擔(dān)心它是否滿足來(lái)自所有其他查詢組件的查詢(特別是如果它是遠(yuǎn)程組件);
缺點(diǎn)
(1)將有幾個(gè)相同數(shù)據(jù)的副本,雖然它們是只讀副本,數(shù)據(jù)存儲(chǔ)在當(dāng)下已經(jīng)不是問(wèn)題;
(2)更高的查詢組件的復(fù)雜性,因?yàn)樗鼘⑿枰壿媮?lái)維護(hù)外部數(shù)據(jù)的本地副本,盡管這是非常標(biāo)準(zhǔn)的邏輯。主從一致性。
如果兩個(gè)組件在同一個(gè)進(jìn)程中(同一個(gè)VM中、同一個(gè)主機(jī)內(nèi))執(zhí)行,這種模式也許沒(méi)有必要,但即使這樣,它也可能很有趣,可以將其用于解耦和可維護(hù)性,或作為將這些組件分離到不同的微服務(wù)中工作做準(zhǔn)備,也許在未來(lái)的某個(gè)時(shí)候我們能平滑升級(jí)到微服務(wù)。這一切都取決于我們目前的需求,未來(lái)的需求。
三。 事件溯源
我們假設(shè)一個(gè)實(shí)體處于一種初始狀態(tài)。作為一個(gè)實(shí)體,它有自己的身份,代表在現(xiàn)實(shí)世界中一個(gè)特定的事情,應(yīng)用程序?qū)⑵浣閷?shí)體。在其生命周期中,實(shí)體數(shù)據(jù)會(huì)發(fā)生變化,并且傳統(tǒng)上實(shí)體的當(dāng)前狀態(tài)被簡(jiǎn)單地作為表的一行記錄存儲(chǔ)在數(shù)據(jù)庫(kù)中。
(1)事務(wù)日志
這在大多數(shù)情況下都是可以的,但如果我們需要知道實(shí)體是如何達(dá)到當(dāng)前這個(gè)狀態(tài),即我們想知道我們的銀行賬戶的貸方和借記發(fā)生的每筆金額,才能知道當(dāng)前賬戶的余額來(lái)歷,這在傳統(tǒng)只保存當(dāng)前狀態(tài)的方式下是不可能實(shí)現(xiàn)的,因?yàn)槲覀冎淮鎯?chǔ)當(dāng)前狀態(tài)!每次都是新的余額狀態(tài)覆蓋了之前的狀態(tài),比如當(dāng)前余額是10,覆蓋了之前余額90,至于賬戶余額怎么剩余10元呢?如果數(shù)據(jù)庫(kù)不保存往來(lái)明細(xì),你可能認(rèn)為銀行系統(tǒng)出問(wèn)題了。
存儲(chǔ)實(shí)體發(fā)生的事件,而不是存儲(chǔ)Entity狀態(tài),我們專注于存儲(chǔ)實(shí)體狀態(tài)更改并從這些更改中計(jì)算實(shí)體狀態(tài)。每個(gè)狀態(tài)變化是一個(gè)事件,存儲(chǔ)在事件流中(即RDBMS中的一個(gè)表)。當(dāng)我們需要實(shí)體的當(dāng)前狀態(tài)時(shí),我們從事件流中的所有事件中計(jì)算出它。
事件存儲(chǔ)成為真相的主要來(lái)源,系統(tǒng)狀態(tài)純粹源于它。對(duì)于程序員來(lái)說(shuō),最好的例子是版本控制系統(tǒng)。所有提交的日志是事件存儲(chǔ),源樹的工作副本是系統(tǒng)狀態(tài)。---2010年Greg Young, CQRS文件
(2)如何刪除?
如果我們發(fā)現(xiàn)一個(gè)狀態(tài)改變(事件)是一個(gè)錯(cuò)誤,我們不能簡(jiǎn)單地刪除該事件,因?yàn)檫@會(huì)改變狀態(tài)更改歷史記錄,這將違反整個(gè)事情溯源的想法。相反,我們?cè)谑录髦袆?chuàng)建一個(gè)事件,以反轉(zhuǎn)我們要?jiǎng)h除的事件。這個(gè)過(guò)程稱為反轉(zhuǎn)事務(wù),不僅使實(shí)體返回到所需的狀態(tài),而且留下了一個(gè)跟蹤,顯示對(duì)象在給定時(shí)間點(diǎn)處于該狀態(tài)。
不刪除數(shù)據(jù)也具有架構(gòu)優(yōu)勢(shì)。存儲(chǔ)系統(tǒng)成為只添加一個(gè)體系結(jié)構(gòu),眾所周知,僅附加體系結(jié)構(gòu)比更新架構(gòu)更容易分發(fā),因?yàn)橐幚淼逆i少得多。---2010年Greg Young, CQRS文件
(3)快照
但是,當(dāng)事件流中有許多事件時(shí),計(jì)算實(shí)體狀態(tài)將是非常昂貴的,因此為了避免出現(xiàn)這種情況。每X個(gè)事件我們將在該時(shí)刻創(chuàng)建一個(gè)實(shí)體狀態(tài)的快照。這樣,當(dāng)我們需要實(shí)體狀態(tài)時(shí),我們只需要計(jì)算它到最后一個(gè)快照。我們甚至可以永久保存更新實(shí)體的快照,這樣我們平衡了兩種世界(只保存狀態(tài)和只保存事件)。
(4)投影預(yù)測(cè)Projections
在事件采集中,我們也有一個(gè)投影的概念,即事件流中的事件的計(jì)算,從特定時(shí)刻開(kāi)始。這意味著快照或?qū)嶓w的當(dāng)前狀態(tài)符合預(yù)測(cè)的定義。但是在預(yù)測(cè)概念中最有價(jià)值的想法是,我們可以在特定時(shí)期分析實(shí)體的“行為”,這使我們能夠?qū)ξ磥?lái)作出有根據(jù)的猜測(cè)(即如果在過(guò)去的5年中,實(shí)體有8月份的活動(dòng)增加,明年8月份可能會(huì)發(fā)生同樣的事情),這對(duì)業(yè)務(wù)來(lái)說(shuō)可能是非常有價(jià)值的。
(5)利弊
事件溯源對(duì)于業(yè)務(wù)和開(kāi)發(fā)過(guò)程都是非常有用的:
1.我們查詢這些事件,用于業(yè)務(wù)和開(kāi)發(fā),以了解用戶和系統(tǒng)行為(調(diào)試);
2.我們還可以使用事件日志重建過(guò)去的狀態(tài),對(duì)于業(yè)務(wù)和開(kāi)發(fā)來(lái)說(shuō)都是有用的;
3.自動(dòng)調(diào)整狀態(tài)以應(yīng)對(duì)追溯變化,非常適合企業(yè)需要頻繁變化;
4.通過(guò)在重播時(shí)注入假想事件來(lái)探索其他歷史,令人敬畏。
但不是一切都是好消息,要注意隱藏的問(wèn)題:
1.外部更新
當(dāng)我們的事件在外部系統(tǒng)中觸發(fā)更新時(shí),但是我們又在重播事件以便創(chuàng)建投影,因此我們不想重新觸發(fā)這些事件。在這一點(diǎn)上,當(dāng)我們處于“重播模式”時(shí),我們可以簡(jiǎn)單地禁用外部更新,也可以將該邏輯封裝在網(wǎng)關(guān)中。
另一個(gè)解決方案取決于實(shí)際問(wèn)題,可能是將外部系統(tǒng)更新放入緩沖,在一段時(shí)間后執(zhí)行,保證事件不會(huì)被重播時(shí)再進(jìn)行。
2.外部查詢
當(dāng)我們的事件使用對(duì)外部系統(tǒng)的查詢,如獲得股票評(píng)級(jí),當(dāng)我們重播事件以創(chuàng)建投影時(shí)會(huì)發(fā)生什么?我們可能希望獲得與事件在第一次運(yùn)行時(shí)所使用的相同的等級(jí),也許是在幾年前。因此,遠(yuǎn)程應(yīng)用程序可以給我們這些值,或者我們需要將它們存儲(chǔ)在系統(tǒng)中,所以我們可以通過(guò)在網(wǎng)關(guān)中封裝該邏輯來(lái)模擬遠(yuǎn)程查詢。
3.代碼更改
Martin Fowler發(fā)現(xiàn)3種類型的代碼更改:新功能,錯(cuò)誤修復(fù)和時(shí)間邏輯。當(dāng)在不同的時(shí)間點(diǎn)播放應(yīng)該使用不同業(yè)務(wù)邏輯規(guī)則的事件時(shí),真正的問(wèn)題就出現(xiàn)。去年的稅收計(jì)算與今年不同。像往常一樣,條件邏輯可以使用,但它會(huì)變得凌亂,所以建議使用策略模式。
所以,我建議謹(jǐn)慎對(duì)待,盡可能遵循這些規(guī)則:
1.保持事態(tài)愚蠢,只關(guān)心狀態(tài)的變化,而不是如何被改變的。這樣我們可以安全地重播任何事件,并期望結(jié)果是一樣的,即使業(yè)務(wù)規(guī)則同時(shí)發(fā)生變化(盡管我們需要保留舊的業(yè)務(wù)規(guī)則,以便我們可以在重播過(guò)去的事件時(shí)應(yīng)用它們);
2.與外部系統(tǒng)的交互不應(yīng)該依賴于這些事件,這樣我們可以安全地重播事件,而不會(huì)重新觸發(fā)外部邏輯,我們不需要確保來(lái)自外部系統(tǒng)的回復(fù)與最初的事件相同。
當(dāng)然,像其他任何模式一樣,我們不需要在任何地方使用它,我們應(yīng)該使用它在哪里是有意義的,它給我們帶來(lái)了一個(gè)優(yōu)勢(shì),并解決了比創(chuàng)建更多的問(wèn)題。
結(jié)論
這又是關(guān)于封裝,低耦合和高凝聚的問(wèn)題。
事件可以平衡代碼的可維護(hù)性、性能和擴(kuò)展性,事件溯源也是系統(tǒng)數(shù)據(jù)可提供的可靠性和信息。
然而,這是一條存在自身危險(xiǎn)的道路,因?yàn)楦拍詈图夹g(shù)的復(fù)雜性都會(huì)增加,而且任何一種的濫用都會(huì)產(chǎn)生災(zāi)難性的后果。
來(lái)源:tuicool
評(píng)論
查看更多