領域驅動設計戰術模式--領域事件

文心紫竹發表於2019-04-16

使用領域事件來捕獲發生在領域中的一些事情。

領域驅動實踐者發現他們可以通過了解更多發生在問題域中的事件,來更好的理解問題域。這些事件,就是領域事件,主要是與領域專家一起進行知識提煉環節中獲得。

領域事件,可以用於一個限界上下文內的領域模型,也可以使用訊息佇列在限界上下文間進行非同步通訊。

1 理解領域事件

領域事件是領域專家所關心的發生在領域中的一些事件。

將領域中所發生的活動建模成一系列離散事件。每個事件都用領域物件表示。領域事件是領域模型的組成部分,表示領域中所發生的事情。

領域事件的主要用途:

  • 保證聚合間的資料一致性
  • 替換批量處理
  • 實現事件源模式
  • 進行限界上下文整合

事件主要用途

2 實現領域事件

領域事件表示已經發生的某種事實,該事實在發生後便不會改變。因此,領域事件通常建模成值物件。

但,這也有特殊的情況,為了迎合序列化和反序列化框架需求,在建模時,經常會進行一定的妥協。

2.1 建立領域事件

2.1.1 事件命名

在建模領域事件時,我們應該根據限界上下文中的通用語言來命名事件。

如果事件由聚合上的命令操作產生,通常根據該操作方法的名字來命名事件。事件名字表明聚合上的命令方法在執行成功後的事實。即事件命名需要反映過去發生過的事情。

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {
    public AccountEnabledEvent(Account source) {
        super(source);
    }
}
複製程式碼
2.1.2 事件屬性

事件的屬性主要用於驅動後續業務流程。當然,也會擁有一些通用屬性。

事件具有一些通用屬性,如:

  • 唯一標識
  • occurredOn 發生時間
  • type 事件型別
  • source 事件發生源(只針對由聚合產生的事件)

通用屬性可以使用事件介面來規範。

事件通用屬性

介面或類 含義
DomainEvent 通用領域事件介面
AggregateEvent 由聚合釋出的通用領域事件介面
AbstractDomainEvent DomainEvent 實現類,維護 id 和 建立時間
AbstractAggregateEvent AggregateEvent 實現類,繼承子 AbstractDomainEvent,並新增 source 屬性

但,事件最主要的還是業務屬性。我們需要考慮,是誰導致事件的發生,這可能涉及產生事件的聚合或其他參與該操作的聚合,也可能是其他任何型別的運算元據。

2.1.3 事件方法

事件是事實的描述,本身不會有太多的業務操作。

領域事件通常被設計為不變物件,事件所攜帶的資料已經反映出該事件的來源。事件建構函式完成狀態初始化,同時提供屬性的 getter 方法。

2.1.4 事件唯一標識

這裡需要注意的是事件唯一標識,通常情況下,事件是不可變的,那為什麼會涉及唯一標識的概念呢?

對於從聚合中釋出出來的領域事件,使用事件的名稱、產生事件的標識、事件發生的時間等足以對不同的事件進行區分。但,這樣會增加事件比較的複雜性。

對於由呼叫方釋出的事件,我們將領域事件建模成聚合,可以直接使用聚合的唯一標識作為事件的標識。

事件唯一標識的引入,會大大減少事件比較的複雜性。但,其最大的意義在於限界上下文的整合。

當我們需要將領域事件釋出到外部的限界上下文時,唯一標識就是一種必然。為了保證事件投遞的冪等性,在傳送端,我們可能會進行多次傳送嘗試,直至明確傳送成功為止;而在接收端,當接收到事件後,需要對事件進行重複性檢測,以保障事件處理的冪等性。此時,事件的唯一標識便可以作為事件去重的依據。

事件唯一標識,本身對領域建模影響不大,但對技術處理好處巨大。因此,將它作為通用屬性進行管理。

2.2 釋出領域事件

我們如何避免領域事件與處理者間的耦合呢?

一種簡單高效的方式便是使用觀察者模式,這種模式可以在領域事件和外部元件間進行解耦。

2.2.1 釋出訂閱模型

為了統一,我們需要定義了一套介面和實現類,以基於觀察者模式,完成事件的釋出。

釋出領域事件

涉及介面和實現類如下:

介面或類 含義
DomainEventPublisher 用於釋出領域事件
DomainEventHandlerRegistry 用於註冊 DomainEventHandler
DomainEventBus 擴充套件自 DomainEventPublisher 和 DomainEventHandlerRegistry 用於釋出和管理領域事件處理器
DefaultDomainEventBus DomainEventBus 預設實現
DomainEventHandler 用於處理領域事件
DomainEventSubscriber 用於判斷是否接受領域事件
DomainEventExecutor 用於執行領域事件處理器

使用例項如 DomainEventBusTest 所示:

public class DomainEventBusTest {
    private DomainEventBus domainEventBus;

    @Before
    public void setUp() throws Exception {
        this.domainEventBus = new DefaultDomainEventBus();
    }

    @After
    public void tearDown() throws Exception {
        this.domainEventBus = null;
    }

    @Test
    public void publishTest(){
        // 建立事件處理器
        TestEventHandler eventHandler = new TestEventHandler();
        // 註冊事件處理器
        this.domainEventBus.register(TestEvent.class, eventHandler);

        // 釋出事件
        this.domainEventBus.publish(new TestEvent("123"));

        // 檢測事件處理器是夠執行
        Assert.assertEquals("123", eventHandler.data);
    }

    @Value
    class TestEvent extends AbstractDomainEvent{
        private String data;
    }

    class TestEventHandler implements DomainEventHandler<TestEvent>{
        private String data;
        @Override
        public void handle(TestEvent event) {
            this.data = event.getData();
        }
    }
}
複製程式碼

在構建完釋出訂閱結構後,需要將其與領域模型進行關聯。領域模型如何獲取 Publisher,事件處理器如何進行訂閱。

2.2.2 基於 ThreadLocal 的事件釋出

比較常用的方案便是將 DomainEventBus 繫結到執行緒上下文。這樣,只要是同一呼叫執行緒都可以方便的獲取 DomainEventBus 物件。

具體的互動如下:

基於 ThreadLocal 的事件釋出流程

DomainEventBusHolder 用於管理 DomainEventBus。

public class DomainEventBusHolder {
    private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){
        @Override
        protected DomainEventBus initialValue() {
            return new DefaultDomainEventBus();
        }
    };

    public static DomainEventPublisher getPubliser(){
        return THREAD_LOCAL.get();
    }

    public static DomainEventHandlerRegistry getHandlerRegistry(){
        return THREAD_LOCAL.get();
    }

    public static void clean(){
        THREAD_LOCAL.remove();
    }
}
複製程式碼

Account 的 enable 直接使用 DomainEventBusHolder 進行釋出。

public class Account extends JpaAggregate {

    public void enable(){
        AccountEnabledEvent event = new AccountEnabledEvent(this);
        DomainEventBusHolder.getPubliser().publish(event);
    }
}

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {
    public AccountEnabledEvent(Account source) {
        super(source);
    }
}
複製程式碼

AccountApplication 完成訂閱器註冊以及業務方法呼叫。

public class AccountApplication extends AbstractApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);

    @Autowired
    private AccountRepository repository;

    public void enable(Long id){
        // 清理之前繫結的 Handler
        DomainEventBusHolder.clean();

        // 註冊 EventHandler
        AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();
        DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);

        Optional<Account> accountOptional = repository.getById(id);
        if (accountOptional.isPresent()) {
            Account account = accountOptional.get();
            // enable 使用 DomainEventBusHolder 直接釋出事件
            account.enable();
            repository.save(account);
        }
    }
    
    class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{

        @Override
        public void handle(AccountEnabledEvent event) {
            LOGGER.info("handle enable event");
        }
    }
}
複製程式碼
2.2.3 基於實體快取的事件釋出

先將事件快取在實體中,在實體狀態成功持久化到儲存後,再進行事件釋出。

具體互動如下:

基於實體快取的事件釋出

例項程式碼如下:

public class Account extends JpaAggregate {

    public void enable(){
        AccountEnabledEvent event = new AccountEnabledEvent(this);
        registerEvent(event);
    }
}
複製程式碼

Accountenable 方法,呼叫 registerEvent 對事件進行註冊。

@MappedSuperclass
public abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class);

    @JsonIgnore
    @QueryTransient
    @Transient
    @org.springframework.data.annotation.Transient
    private final transient List<DomainEventItem> events = Lists.newArrayList();

    protected void registerEvent(DomainEvent event) {
        events.add(new DomainEventItem(event));
    }

    protected void registerEvent(Supplier<DomainEvent> eventSupplier) {
        this.events.add(new DomainEventItem(eventSupplier));
    }

    @Override
    @JsonIgnore
    public List<DomainEvent> getEvents() {
        return Collections.unmodifiableList(events.stream()
                .map(eventSupplier -> eventSupplier.getEvent())
                .collect(Collectors.toList()));
    }

    @Override
    public void cleanEvents() {
        events.clear();
    }


    private class DomainEventItem {
        DomainEventItem(DomainEvent event) {
            Preconditions.checkArgument(event != null);
            this.domainEvent = event;
        }

        DomainEventItem(Supplier<DomainEvent> supplier) {
            Preconditions.checkArgument(supplier != null);
            this.domainEventSupplier = supplier;
        }

        private DomainEvent domainEvent;
        private Supplier<DomainEvent> domainEventSupplier;

        public DomainEvent getEvent() {
            if (domainEvent != null) {
                return domainEvent;
            }
            DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null;
            domainEvent = event;
            return domainEvent;
        }
    }
}

複製程式碼

registerEvent 方法在 AbstractAggregate 中,registerEvent 方法將事件儲存到 events 集合,getEvents 方法獲取所有事件,cleanEvents 方法清理快取的事件。

Application 例項如下:

@Service
public class AccountApplication extends AbstractApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);

    @Autowired
    private AccountRepository repository;

    @Autowired
    private DomainEventBus domainEventBus;

    @PostConstruct
    public void init(){
        // 使用 Spring 生命週期註冊事件處理器
        this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());
    }

    public void enable(Long id){
        Optional<Account> accountOptional = repository.getById(id);
        if (accountOptional.isPresent()) {
            Account account = accountOptional.get();
            // enable 將事件快取在 account 中
            account.enable();
            repository.save(account);
            List<DomainEvent> events = account.getEvents();
            if (!CollectionUtils.isEmpty(events)){
                // 成功持久化後,對事件進行釋出
                this.domainEventBus.publishAll(events);
            }
        }
    }

    class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{

        @Override
        public void handle(AccountEnabledEvent event) {
            LOGGER.info("handle enable event");
        }
    }
}
複製程式碼

AccountApplicationinit 方法完成事件監聽器的註冊,enable 方法在實體成功持久化後,將快取的事件通過 DomainEventBus 例項 publish 出去。

2.2.4 由呼叫方釋出事件

通常情況下,領域事件是由聚合的命令方法產生,並在命令方法執行成功後,進行事件的釋出。 有時,領域事件並不是聚合中的命令方法產生的,而是由使用者所發生的請求產生。

此時,我們需要將領域事件建模成一個聚合,並且擁有自己的資源庫。但,由於領域事件表示的是過去發生的事情,因此資源庫只做追加操作,不能對事件進行修改和刪除功能。

例如,對使用者點選事件進行釋出。

@Entity
@Data
public class ClickAction extends JpaAggregate implements DomainEvent {
    @Setter(AccessLevel.PRIVATE)
    private Long userId;
    @Setter(AccessLevel.PRIVATE)
    private String menuId;

    public ClickAction(Long userId, String menuId){
        Preconditions.checkArgument(userId != null);
        Preconditions.checkArgument(StringUtils.isNotEmpty(menuId));

        setUserId(userId);
        setMenuId(menuId);
    }

    @Override
    public String id() {
        return String.valueOf(getId());
    }

    @Override
    public Date occurredOn() {
        return getCreateTime();
    }

}
複製程式碼

ClickAction 繼承自 JpaAggregate 實現 DomainEvent 介面,並重寫 id 和 occurredOn 方法。

@Service
public class ClickActionApplication extends AbstractApplication {
    @Autowired
    private ClickActionRepository repository;

    @Autowired
    private DomainEventBus domainEventBus;

    public void clickMenu(Long id, String menuId){
        ClickAction clickAction = new ClickAction(id, menuId);
        clickAction.prePersist();
        this.repository.save(clickAction);
        domainEventBus.publish(clickAction);
    }
}
複製程式碼

ClickActionApplication 在成功儲存 ClickAction 後,使用 DomainEventBus 對事件進行釋出。

2.3 訂閱領域事件

由什麼元件向領域事件註冊訂閱器呢?大多數請求,由應用服務完成,有時也可以由領域服務進行註冊。

由於應用服務是領域模型的直接客戶,它是註冊領域事件訂閱器的理想場所,即在應用服務呼叫領域方法之前,就完成了對事件的訂閱。

基於 ThreadLocal 進行訂閱:

public void enable(Long id){
    // 清理之前繫結的 Handler
    DomainEventBusHolder.clean();

    // 註冊 EventHandler
    AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();
    DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);

    Optional<Account> accountOptional = repository.getById(id);
    if (accountOptional.isPresent()) {
        Account account = accountOptional.get();
        // enable 使用 DomainEventBusHolder 直接釋出事件
        account.enable();
        repository.save(account);
    }
}
複製程式碼

基於實體快取進行訂閱:

@PostConstruct
public void init(){
    // 使用 Spring 生命週期註冊事件處理器
    this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());
}

public void enable(Long id){
    Optional<Account> accountOptional = repository.getById(id);
    if (accountOptional.isPresent()) {
        Account account = accountOptional.get();
        // enable 將事件快取在 account 中
        account.enable();
        repository.save(account);
        List<DomainEvent> events = account.getEvents();
        if (!CollectionUtils.isEmpty(events)){
            // 成功持久化後,對事件進行釋出
            this.domainEventBus.publishAll(events);
        }
    }
}
複製程式碼

2.4 處理領域事件

完成事件釋出後,讓我們一起看下事件處理。

2.4.1 保證聚合間的資料一致性

我們通常將領域事件用於維護模型的一致性。在聚合建模中有一個原則,就是在一個事務中,只能對一個聚合進行修改,由此產生的變化必須在獨立的事務中執行。

在這種情況下,需要謹慎處理的事務的傳播性。

保證聚合間的資料一致性

應用服務控制著事務。不要在事件通知過程中修改另一個聚合例項,因為這樣會破壞聚合的一大原則:在一個事務中,只能對一個聚合進行修改。

對於簡單場景,我們可以使用特殊的事務隔離策略對聚合的修改進行隔離。具體流程如下:

保證聚合間的資料一致性

但,最佳方案是使用非同步處理。及每一個定義方都在各自獨立的事務中修改額外的聚合例項。

事件訂閱方不應該在另一個聚合上執行命令方法,因為這樣將破壞“在單個事務中只修改單個聚合例項”的原則。所有聚合例項間的最終一致性必須通過非同步方式處理。

詳見,非同步處理領域事件。

2.4.2 替換批量處理

批處理過程通常需要複雜的查詢,並且需要龐大的事務支援。如果在接收到領域事件時,系統就立即處理,業務需求不僅得到了更快的滿足,而且杜絕了批處理操作。

使用實時代替批量處理

在系統的非高峰時期,通常使用批處理進行一些系統的維護,比如刪除過期資料、建立新的物件、通知使用者、更新統計資訊等。這些批處理往往需要複雜的查詢,並需要龐大的事務支援。

如果我們監聽系統中的領域事件,在接收領域事件時,系統立即處理。這樣,原本批量集中處理的過程就被分散成許多小的處理單元,業務需要也能更快的滿足,使用者可以可以及時的進行下一步操作。

使用實時代替批量處理

2.4.3 實現事件源模式

對於單個限界上下文中的所有領域事件,為它們維護一個事件儲存具有很多的好處。

對事件進行儲存可以:

  • 將事件儲存作為訊息佇列來使用,然後將領域事件通過訊息設施釋出出去。
  • 將事件儲存用於基於 Rest 的事件通知。
  • 檢查模型命名方法產生結果的歷史記錄。
  • 使用事件儲存來進行業務預測和分析。
  • 使用事件來重建聚合例項。
  • 執行聚合的撤銷操作。

實現事件源模型

事件儲存是個比較大的課題,將有專門章節進行講解。

2.4.4 進行限界上下文整合

基於領域事件的限界上下文整合,主要由訊息佇列和 REST 事件兩種模式。

在此,重心講解基於訊息佇列的上下文整合。

在不同的上下文中採用訊息系統時,我們必須保證最終一致性。在這種情況下,我們至少需要在兩種儲存之間儲存最終一致性:領域模型所使用的儲存和訊息佇列所使用的持久化儲存。我們必須保證在持久化領域模型時,對於的事件也已經成功釋出。如果兩種不同步,模型可能會處於不正確的狀態。

一般情況下,有三種方式:

  1. 領域模型和訊息共享持久化儲存。在這種情況下,模型和事件的提交在一個事務中完成,從而保證兩種的一致性。
  2. 領域模型和訊息由全域性事務控制。這種情況下,模型和訊息所用的持久化儲存可以分離,但會降低系統效能。
  3. 在領域持久化儲存中,建立一個特殊的儲存區域用於儲存事件(也就是事件儲存),從而在本地事務中完成領域和事件的儲存。然後,通過後臺服務將事件非同步傳送到訊息佇列中。

一般情況下,第三種,是比較優雅的解決方案。

進行限界上下文整合

在一致性要求不高時,可以通過領域事件訂閱器直接向訊息佇列傳送事件。具體流程如下:

進行限界上下文整合

對一致性要求高時,需要先將事件儲存,然後通過後臺執行緒載入並分發到訊息佇列。具體流程如下:

進行限界上下文整合

2.5 非同步處理領域事件

領域事件可以與非同步工作流程協同,包括限界上下文間使用訊息佇列進行非同步通訊。當然,在同一個限界上下文中,也可以啟動非同步處理流程。

作為事件的釋出者,不應關心是否執行非同步處理。異常處理是由事件執行者決定。

DomainEventExecutor 提供對非同步處理的支援。

DomainEventExecutor eventExecutor  =
                new ExecutorBasedDomainEventExecutor("EventHandler", 1, 100);
this.domainEventBus.register(AccountEnabledEvent.class,
        eventExecutor,
        new AccountEnableEventHandler());
複製程式碼

非同步處理,就意味著放棄資料庫事務的 ACID 特性,而選擇使用最終一致性。

2.6 內部事件與外部事件

使用領域事件時需要對事件進行區分,以避免技術實現的問題。

認識內部事件和外部事件之間的區別至關重要。

  • 內部事件,是一個領域模型內部的事件,不在有界上下文間進行共享。
  • 外部事件,是對外發布的事件,在多個有界上下文中進行共享。

內部事件與外部事件

一般情況下,在典型的業務用例中,可能會有很多的內部事件,而只有一兩個外部事件。

2.6.1 內部事件

內部事件存在於限界上下文內部,受限界上下文邊界保護。

內部事件被限制在單個有界上下文邊界內部,所以可以直接引用領域物件。

public interface AggregateEvent<ID, A extends Aggregate<ID>> extends DomainEvent{
    A source();

    default A getSource(){
        return source();
    }
}
複製程式碼

比如 AggregateEvent 中的 source 指向釋出該事件的聚合。

public class LikeSubmittedEvent extends AbstractAggregateEvent<Long, Like> {
    public LikeSubmittedEvent(Like source) {
        super(source);
    }

    public LikeSubmittedEvent(String id, Like source) {
        super(id, source);
    }
}
複製程式碼

LikeSubmittedEvent 類直接引用 Like 聚合。

2.6.2 外部事件

外部事件存在於限界上下文間,被多個上下文共享。

一般情況下,外部事件,只作為資料載體存在。常常採用平面結構,並公開所有屬性。

@Data
public class SubmittedEvent {
    private Owner owner;
    private Target target;
}
複製程式碼

SubmittedEvent 為扁平化結構,主要是對資料的封裝。

由於外部事件被多個上下文共享,版本管理就顯得非常重要,以避免重大更改對其服務造成影響。

3 實現領域事件模式

領域事件是一種通用模式,它的本質是將領域概念新增到釋出-訂閱模式。

3.1 封裝領域事件的“釋出-訂閱”模式

釋出-訂閱是比較成熟的設計模式,具有很高的通用性。因此,建議針對領域需求進行封裝。

比如直接使用 geekhalo-ddd 相關模組。

定義領域事件:

@Value
public class LikeCancelledEvent extends AbstractAggregateEvent<Long, Like> {
    public LikeCancelledEvent(Like source) {
        super(source);
    }
}
複製程式碼

訂閱領域事件:

this.domainEventBus.register(LikeCancelledEvent.class, likeCancelledEvent->{
        CanceledEvent canceledEvent = new CanceledEvent();
        canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
        canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(canceledEvent);
    });
複製程式碼

非同步執行領域事件:

DomainEventExecutor eventExecutor  =
                new ExecutorBasedDomainEventExecutor("LikeEventHandler", 1, 100);
this.domainEventBus.register(LikeCancelledEvent.class, 
        eventExecutor, 
        likeCancelledEvent->{
            CanceledEvent canceledEvent = new CanceledEvent();
            canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
            canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
            this.redisBasedQueue.pushLikeEvent(canceledEvent);
    });
複製程式碼

3.2 記憶體匯流排處理內部事件,訊息佇列處理外部事件

記憶體匯流排簡單高效,同時支援同步、非同步兩個處理方案,比較適合處理繁雜的內部事件;訊息佇列雖然複雜,但擅長解決服務間通訊問題,適合處理外部事件。

3.3 使用實體快取領域事件

理論上,只有在業務成功完成後,才應該對外發布事件。因此,將領域事件快取在實體中,並在完成業務操作後將其進行釋出,是一種較好的解決方案。

相比,使用 ThreadLocal 管理訂閱器,並在事件 publish 時進行訂閱回撥,事件快取方案有明顯的優勢。

3.4 使用 IOC 容器的事件釋出功能

IOC 容器為我們提供了很多使用功能,其中也包括髮布-訂閱功能,如 Spring。

通常情況下,領域模型不應該直接依賴於 Spring 容器。因此,在領域中我們仍然使用記憶體匯流排,為其新增一個訂閱者,將記憶體匯流排中的事件轉發到 Spring 容器中。

Spring 事件釋出

class SpringEventDispatcher implements ApplicationEventPublisherAware {

    @Autowired
    private DomainEventBus domainEventBus;

    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }

    @PostConstruct
    public void addListener(){
        this.domainEventBus.register(event->true, event -> {this.eventPublisher.publishEvent(event);});
    }

}
複製程式碼

此時,我們就可以直接使用 Spring 的 EventListener 機制對領域事件進行處理。

@Component
public class RedisBasedQueueExporter {
    @Autowired
    private RedisBasedQueue redisBasedQueue;

    @EventListener
    public void handle(LikeSubmittedEvent likeSubmittedEvent){
        SubmittedEvent submittedEvent = new SubmittedEvent();
        submittedEvent.setOwner(likeSubmittedEvent.getSource().getOwner());
        submittedEvent.setTarget(likeSubmittedEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(submittedEvent);
    }


    @EventListener
    public void handle(LikeCancelledEvent likeCancelledEvent){
        CanceledEvent canceledEvent = new CanceledEvent();
        canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
        canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(canceledEvent);
    }

}
複製程式碼

4 小結

  • 領域事件是發生在問題域中的事實,它是通用語言的一部分。
  • 領域事件優先使用釋出-訂閱模式,會發布事件並且觸發相應的事件處理器。
  • 限界上下文內,優先使用內部事件和記憶體匯流排;限界上下文間,優先使用外部事件和訊息佇列。
  • 領域事件使非同步操作變得簡單。
  • 領域事件為聚合間提供了最終一致性。
  • 領域事件可以將大的批量操作簡化為許多小的業務操作。
  • 領域事件可以完成強大的事件儲存。
  • 領域事件可以完成限界上下文間的整合。
  • 領域事件是更復雜架構(CQRS)的一種支援。

相關文章