<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為 Gopher 打造 DDD 系列:領(lǐng)域模型-領(lǐng)域事件

          共 5587字,需瀏覽 12分鐘

           ·

          2020-12-22 13:56

          前言:?在DDD中,一個業(yè)務(wù)用例對應(yīng)一個事務(wù),一個事務(wù)對應(yīng)一個聚合根,在一次事務(wù)中,只能對一個聚合根進(jìn)行操作。那么在復(fù)雜的業(yè)務(wù)場景涉及多個聚合根的修改,特別是許多聚合根處于不同的限界上下文中時,我們可以選擇使用領(lǐng)域事件對其進(jìn)行修改。

          使用freedom,兩行代碼就讓你輕松搞定領(lǐng)域事件!

          一、DomainEvent

          什么是領(lǐng)域事件

          領(lǐng)域事件是領(lǐng)域驅(qū)動設(shè)計中的一個重要概念,我們使用領(lǐng)域事件來捕獲領(lǐng)域中發(fā)生的一些事情。只有那些對業(yè)務(wù)有價值,能夠有助于形成完整的業(yè)務(wù)閉環(huán),能夠推動下一步業(yè)務(wù)發(fā)展的事情,才能被當(dāng)做領(lǐng)域事件。

          這里舉一個例子,當(dāng)用戶在訂單服務(wù)下單付款后,倉儲物流團(tuán)隊就可以開始進(jìn)行發(fā)貨流程發(fā)貨了。當(dāng)用戶確認(rèn)收貨后,發(fā)票服務(wù)的會計團(tuán)隊就能開始做賬報稅開票了。這一系列的動作都是屬于事件,屬于各自領(lǐng)域的事件,并且明顯的能夠推動業(yè)務(wù)的發(fā)展!

          隨著微服務(wù)的興起,拆分服務(wù)和事件風(fēng)暴在不斷的演進(jìn),領(lǐng)域事件扮演著靈魂角色。在事件風(fēng)暴中,發(fā)現(xiàn)并提取領(lǐng)域事件,將以領(lǐng)域事件為中心的業(yè)務(wù)模型,演化成以聚合為中心的領(lǐng)域模型,是DDD落地實踐的一種重要手段。

          領(lǐng)域事件是屬于領(lǐng)域模型的,前面我們已經(jīng)介紹過聚合實體都是領(lǐng)域模型。在領(lǐng)域事件建模時,我們應(yīng)該關(guān)注這一點。用戶實體修改密碼?訂單聚合支付?訂單聚合發(fā)貨?它們完成命令后是否應(yīng)該發(fā)布通知?

          二、事件的存儲

          在數(shù)據(jù)的最終一致性的問題上,我們至少要保證該限界上下文內(nèi)領(lǐng)域模型的持久化和領(lǐng)域事件的發(fā)布是一致的。

          如果訂單狀態(tài)修改為已付款,然后在使用MQ基礎(chǔ)設(shè)施發(fā)布失敗,那么是不一致的。那么如果是先使用MQ基礎(chǔ)設(shè)施發(fā)布成功,然后在修改訂單狀態(tài)為已付款失敗,也是不一致的。

          我們需要一張事件表和使用本地事務(wù)。在領(lǐng)域模型持久化的同時并插入一條領(lǐng)域事件的記錄,它們依賴本地事務(wù)來保持?jǐn)?shù)據(jù)的一致性。

          當(dāng)事務(wù)成功后再使用MQ基礎(chǔ)設(shè)施發(fā)布通知,如果失敗了,定時器掃描后繼續(xù)發(fā)送直至發(fā)送成功。

          //事務(wù)偽代碼begin  update order set status = PAID  insert into domain_event_publishcommit 
          //事件發(fā)布表CREATE TABLE `domain_event_publish` ( `id` int(11) NOT NULL AUTO_INCREMENT, `topic` varchar(255) NOT NULL, `content` varchar(2000) NOT NULL, PRIMARY KEY (`id`));
          //訂單表CREATE TABLE `order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(65) NOT NULL DEFAULT '', `user_id` int(11) NOT NULL COMMENT '用戶id', `status` enum('PAID','NON_PAYMENT','SHIPMENT','DONE') NOT NULL DEFAULT 'NON_PAYMENT' COMMENT '支付,未支付,發(fā)貨,完成',)
          //事件消費表CREATE TABLE `domain_event_subscribe` ( `id` int(11) NOT NULL AUTO_INCREMENT, `publish_id` int(11) NOT NULL, `topic` varchar(255) NOT NULL, `content` varchar(2000) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `publish_id` (`publish_id`))

          看到這里你可能覺得會很麻煩和復(fù)雜,其實只要代碼設(shè)計的漂亮,這不過是幾行代碼的事情。

          freedom還有一個額外的優(yōu)點,當(dāng)我們事務(wù)成功后,并不一定要用MQ這種基礎(chǔ)設(shè)施去發(fā)布通知,也可以選擇用**HTTP API、RPC,甚至是GolangChannelCond。

          發(fā)布方

          發(fā)布流程

          訂閱方

          消費流程

          三、領(lǐng)域事件的注意事項

          領(lǐng)域事件不僅僅只有Topic?,而且還需要使用唯一身份標(biāo)識符,這樣該事件才是一個冪等的事件。

          通常發(fā)布方用重試用唯一身份標(biāo)識符來識別,訂閱方的冪等消費也用唯一身份標(biāo)識符來識別。上述示例使用了自增的唯一ID,實際生產(chǎn)中建議使用UUID。

          雖然事務(wù)成功立刻發(fā)布消息,定時器只是重試失敗的發(fā)布,但依然會存在發(fā)布和消費的延遲。我們應(yīng)該細(xì)心的評估這種影響。是否可以接受?如果不能接受并且要加入中間狀態(tài)(例如‘訂單確認(rèn)中’),是否產(chǎn)生依賴和耦合?

          領(lǐng)域事件可以解耦微服務(wù),使限界上下文更清晰,并且達(dá)到最終一致。通常聚合根可以引用其他限界上下文Query來的值對象,然后聚合根發(fā)布事件來觸發(fā)其他限界上下文的變更。聚合根是不能組合其他限界上下文的實體,不論是否在一個服務(wù)內(nèi)。

          領(lǐng)域事件也會有陷阱,最常見的是被動操控型命令?!兑?、什么是領(lǐng)域事件》已經(jīng)簡單介紹過命令和事件。

          這里還是舉個例子,當(dāng)用戶為訂單付款后,訂單發(fā)布事件的Topic如果是訂單已支付,這是屬于該訂單領(lǐng)域模型自身發(fā)布的領(lǐng)域事件,這個是正確的。如果Topic是開發(fā)票,訂單這個領(lǐng)域模型讓誰開發(fā)票???這是錯誤的,這就是被動操控型命令(Passive-aggressive command).

          四、代碼實戰(zhàn)

          創(chuàng)建一個修改密碼的領(lǐng)域事件


          package event
          import "encoding/json"
          // ChangePassword 修改密碼事件type ChangePassword struct { ID int `json:"id"` prototypes map[string]interface{} UserID int `json:"userID"` NewPassword string `json:"newPassword"` OldPassword string `json:"oldPassword"`}
          // Topic 返回該事件的Topic.func (password *ChangePassword) Topic() string { return "ChangePassword"}
          // Marshal 序列化.func (password *ChangePassword) Marshal() []byte { data, _ := json.Marshal(password) return data}
          // Identity 返回唯一標(biāo)識.func (password *ChangePassword) Identity() interface{} { return password.ID}
          // SetIdentity 設(shè)置唯一標(biāo)識,通常是事件管理器統(tǒng)一設(shè)置.func (password *ChangePassword) SetIdentity(identity interface{}) { password.ID = identity.(int)}
          在實體修改密碼的行為中使用領(lǐng)域事件
          package entity
          import ( "github.com/8treenet/freedom" "github.com/8treenet/freedom/example/fshop/domain/event" "github.com/8treenet/freedom/example/fshop/domain/po")
          type User struct { freedom.Entity //實體的基類,繼承后可以使用事件集合的方法。 po.User}
          // Identity 返回實體的唯一標(biāo)識func (u *User) Identity() string { return strconv.Itoa(u.ID)}
          // ChangePassword 修改密碼func (u *User) ChangePassword(newPassword, oldPassword string) error { if u.Password != oldPassword { return errors.New("Password error") } u.SetPassword(newPassword)
          //為實體加入修改密碼事件 u.AddPubEvent(&event.ChangePassword{ UserID: u.User.ID, NewPassword: u.Password, OldPassword: oldPassword, }) return nil}

          領(lǐng)域服務(wù)的處理

          import (  "github.com/8treenet/freedom/example/fshop/domain/dependency"  "github.com/8treenet/freedom/example/fshop/domain/entity")
          type UserService struct { UserRepo dependency.UserRepo //依賴倒置用戶資源庫 Transaction *domainevent.EventTransaction //依賴注入事務(wù)組件}
          // ChangePassword 修改密碼func (user *UserService) ChangePassword(userID int, newPassword, oldPassword string) (e error) { //使用資源庫讀取用戶實體 var userEntity *entity.User userEntity, e = user.UserRepo.Get(userID) if e != nil { return }
          //修改密碼 if e = userEntity.ChangePassword(newPassword, oldPassword); e != nil { return }
          //使用事務(wù)組件保證一致性 1.修改密碼屬性, 2.事件表增加記錄 //Execute 如果返回錯誤 會觸發(fā)回滾。成功會調(diào)用infra/domainevent/EventManager.push e = user.Transaction.Execute(func() error { return user.UserRepo.Save(userEntity) }) return}

          資源庫的處理


          package repository
          import ( "github.com/8treenet/freedom/example/fshop/domain/entity" "github.com/8treenet/freedom/example/fshop/infra/domainevent" "github.com/8treenet/freedom")
          type UserRepository struct { freedom.Repository EventRepository *domainevent.EventManager //領(lǐng)域事件組件}// Save .func (repo *UserRepository) Save(entity *entity.User) error { //持久化實體 _, e := saveUser(repo, &entity.User) if e != nil { return e } //持久化事件 return repo.EventRepository.Save(&repo.Repository, entity)}
          領(lǐng)域事件組件介紹
          //領(lǐng)域事件基礎(chǔ)設(shè)施包package domainevent
          // EventManager 實現(xiàn)了通用的Pub/Sub處理,資源庫引入后直接使用Save方法.type EventManager struct {}
          //EventTransaction 配合EventManager的事務(wù)組件.type EventTransaction struct {}
          更多細(xì)節(jié),請參考代碼。https://github.com/8treenet/freedom/blob/master/example/fshop

          目錄

          • golang領(lǐng)域模型-開篇
          • golang領(lǐng)域模型-六邊形架構(gòu)
          • golang領(lǐng)域模型-實體
          • golang領(lǐng)域模型-資源庫
          • golang領(lǐng)域模型-依賴倒置
          • golang領(lǐng)域模型-聚合根
          • golang領(lǐng)域模型-CQRS
          • golang領(lǐng)域模型-領(lǐng)域事件


          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲?。贿€可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天不射视频网站 | 干逼免费视频 | 无码视频久久 | 69人人| 在线观看内射婷婷 |