iT邦幫忙

2024 iThome 鐵人賽

DAY 21
1

同步至 medium

https://ithelp.ithome.com.tw/upload/images/20241005/20089358HDJbuXbwtB.png


上一篇文章中,我們已經談完 event storming 的整個流程後,接下來我們就來看看如何轉成程式碼。

說明一下範例的 Event Storming 結果

這個範例我們用 event storming 跑了一個情境,那就是會員機制,它的幾個需求為 :

  • 會員等級: 這個目前會根據購課金額來決定
  • 每個會員都有任務: 完成任務後,可能會有什麼獎勵。

然後根劇 event storming 的結如下 :

  • Member Bounded Context: 裡面有 Member Aggregate 與 Task Entity,然後它的職責 ( Domain Event ) 如下:
    • 已升級會員等級
    • 已領取升級禮
    • 已綁定 Line
    • 已填寫完會員資訊
    • 已開啟學習計畫
    • 已完成會員任務
  • Purchase Bounded Context: 裡面有 Order Aggregate,然後它的職責 ( Domain Event ) 如下。
    • 已付款

其中我們在 Member BC 有一個 Policy 為當收到已付款時,我們需要計算要不要升級會員。

https://ithelp.ithome.com.tw/upload/images/20241005/20089358ozG6fEZ4tX.png


程式碼化

先前提說明一下,這個比較算是範例碼,很多的細節事實上都沒有提到,但是如果要知道從 event storming 轉成程式碼,那應該只要先這樣就好,接下來後面的文章才會 peak 每個地方。

1. Member Bounded Context 的 Member Aggregate

https://ithelp.ithome.com.tw/upload/images/20241005/20089358462F2glO94.png

這裡有幾個重點 :

  1. 這裡每一個方法都可能會產生對應的 command 與 domain event,就是橘色便利貼,但是先說一下,我這裡的 this.addDomainEvent,就只是先加到一個陣列中,然後外面再統一送,這裡之後會詳細說一下,因為還有不少種實作類型,我自已是偏好這種。
  2. 這個 Aggregate 內所有屬性變化都會封在這個裡面 ( 就等同於 domain model 的概念 )。
  3. 正常來說 domain model 是不會使用外部服務的,但是這裡有一個例外,就是當 domain model 裡的業務事件或欄位和他有關的話就會考慮這樣,像 sendLevelUPGift 我就會考慮,至於這個優缺可以參考以下兩篇 :
 */
class MemberAggregate extends BaseAggregate {
  private id: string;
  private name: string;
  private email: string;
  private lineId: string;
  private tasksMap: Map<
    string,
    ProfileTask | LoginTask | RecommendRegisterTask | LineBindTask
  >;
  private Level: Level;
  private hasSendLevelUpGift: boolean;
  private cumulativeAmount: number;

  constructor() {
    super();
  }

  /**
   * 產生已完成 profile 的 domain event
   */
  public editorProfile(name: string, email: string): void {
    this.name = name;
    this.email = email;

    const task: ProfileTask = this.tasksMap.get(
      Tasks.PROFILE_TASK,
    );
    task.complete(name, email);
    this.addDomainEvent(DomainEvent.PROFILE_EDITOR, {});
  }

  public bindLineId(lineId: string): void {
    this.lineId = lineId;

    const task: LineBindTask = this.tasksMap.get(
      Tasks.LINE_BIND_TASK,
    );

    task.complete(lineId);
    this.addDomainEvent(DomainEvent.BIND_LINE_ID, {});
  }

  public settingStudentPlan(): void {
    // ...
     const task: StudyPlanTask = this.tasksMap.get(
      Tasks.StudyPlanSetting,
    );

    task.complete();
    this.addDomainEvent(DomainEvent.STARTED_STUDENT_PLAN, {});
  }

  private calculateMemberLevel(newOrderAmount: number, notifyService: NofifyService): void {
    if(cumulativeAmount + newOrderAmount > 1000){
      this.Level = Level.L2
      await notifyService.send('level up gift');
      this.hasSendLevelUpGift = true;
      this.addDomainEvent(DomainEvent.MEMBER_LEVEL_UP, {});
    }
    cumulativeAmount += newOrderAmount;
  }
}

2. Member Bounded Context 處理來至其它 Bounded Context 的 Domain Event.

https://ithelp.ithome.com.tw/upload/images/20241005/20089358r6BJoyv8K1.png

然後有幾個重點可以記一下:

  • 在 ddd 實作篇中,有將 service 分成兩個 application service 與 domain service,然後這裡的範例比較接近兩者合一,而且通常我開新專案的話也會先以一個為主,有開始變太複雜時,我才考慮拆分成 application 與 domain 兩個 service。
  • 這裡通常也是取得呼叫 repository 取得 domain model 的地方。
  • 然後由於它有 domain service 的職責,所以有時後我們會去資料庫取得一些資料判斷,也是會在這裡。
class MemberService {
  constructor(
    private memberRepository: MemberRepository,
    private notifyService: NofifyService
  ) {}

  @SubscribeDomainEvent('Purchase:Order:Paid')
  async receiveOrderPaid(order: Order): Promise<void> {
    const member = await this.memberRepository.findMemberById(memberId);

    await member.calculateMemberLevel(order.amount, notifyService);
    await this.memberRepository.save(member);
    await this.domainEventPublisher.publish(member.domainEvents);
  }

3. Bounded Context 的關係

備註: 這個資料夾結構我自已是覺得 DDD 的最簡單與理解 Bounded Context的版本,後面會說個考慮其它架構的版本。

然後下面上面的 dal 不要看,看 module 裡面,然後其中 member 與 purchase 就是我範例中說明 bounded context,然後這有以下幾個重點 :

  1. 每個 module 就是以 bounded context 為單位來當邊界。
  2. 任何跨 module 的溝通,只能透過 domain event + application layer 所提供的 api。
  3. 各 module 有自已的職責與資料表 ( 正常來說是資料庫 )

然後這裡是建議不要每一次切個 BC 就開一個微服務,因為 BC 是會需要迭代改變的,如果太早切那個 BC 遲早就會變成 legacy。

https://ithelp.ithome.com.tw/upload/images/20241005/20089358YYX0qEMH3z.png

~補充 Bounded Context 的設計重點~

  1. 讓各 bounded context 越獨立越好。
  2. 每一個 aggregate 與 enity 所做的事是符合這個 bounded context 的 :
    a. 例如 ( bad ): purchase 的 bounded context 裡面,有個叫 learning aggregate 然後他又有記錄學習時間,就怪怪。
    b. 例如 ( bad ): learning 的 bounded context 有 order 這個 aggregate 可以做業務的也怪怪的。
  3. 以 transaction 為概念來想。
  4. 沒必要別太細,例如一個 subtitle 就一個 bounded context !?
  5. 如果一個 bounded context 需要太多 bounded context 才能完成,工作,那他本身就有點問題,因為他都不能獨立運行。 ( 但平台類是例外 )

Q&A

1. Entity 可以產生 Domain Event 嗎 ?

這個我不確定,但以我的認知是要可以。

2. Actor 去那了呢 ?

這個我自已覺得可以提現在 service 根據不同 actor 來進行切分,而至於要不要切 aggregate 好像也不是說不行。

3. Read Model 去那了呢 ?

之後 CQRS 的章節會提到。


小結

這篇文章中,我們探討了如何將 Event Storming 的結果轉換為程式碼,但實務上還有一些難題還要處理 :

  • 我們要如何維護 Bounded Context 的變動 ?
  • 我們 Event Storming 的結果如何整理與儲放,那之後如果還有變動,是要回去修改 miro 嗎 ? 然後變動的流程是什麼 ? 不太可能 pm 與營運們討論完就改 miro,但程式碼沒改到。
  • 有沒有辦法可以讓 Event Storming 文件與程式碼連動 ?

我自已覺得如果只是要實作 event storming 到程式碼中,不能說到很難,我覺得真正難的是後面如何整到流程,並且還有文件的變動如何管理才叫難事。


上一篇
Day-20: Event Storming 經驗談
下一篇
Day-22: 好的軟體架構的特點 ( Base Clean Architecture + DDD )
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言