基於Elixir使用Phoenix建立CQRS/ES應用

banq發表於2017-01-07
該文介紹了 Segment Challenge 是如何使用命令查詢責任分離CQRS事件溯源模式建立其Web應用。

使用Elixir遵循領域驅動設計使用CQRS非常自然,包括使用Erlang的Actor模型,聚合根非常適合Elixir中的Process,使用不可變的訊息驅動互動,彼此隔離並行執行,透過自己的訊息mailbox實現訪問控制。

關於Segment Challenge背景:
如果你是一個熱衷於騎腳踏車的喜歡運動的人,那麼你會知道Strava。它是一個運動者的社交網路。記錄了他們的騎乘和跑步日誌,並將日誌上傳到該網站。

Strava使用者根據路線中一段段建立記錄分段的。例如,一個記錄段包括爬上一座山;從底部開始爬並在頂部完成。每個記錄段都有自己的排行榜。顯示了已經跑過它的每個運動員的排名。最快的人是山的國王(KOM),最快的女人是山的女王(QOM)。運動員可以與其他沿著相同路線運動的Strava使用者進行比較。

這種分段挑戰允許運動員為腳踏車俱樂部及其成員建立比賽。每個月使用不同的Strava段。基於每個運動員在階段結束時的位置來累積積分。該網站使用Strava的API來獲取俱樂部成員的分段成績。排列成績,並在每個階段結束時公佈他們的積分。這種方式替換了在電子表格中手動跟蹤此類資訊的繁瑣。

該網站是完全自助服務。任何註冊的Strava使用者都可以為他們所屬的腳踏車俱樂部建立和主持挑戰。它在2016年年底部署,現在正在為三個地方俱樂部舉辦積極的挑戰。

Segment Challenge這個聚合根是需要來跟蹤每個分段挑戰,稱為Challenge,其有公開命令方法create_challenge接受挑戰的狀態和一個命令,返回零或一個或多個領域事件

聚合根必須保護自己防止外部命令導致內部不變性的破壞,比如,試圖啟動一個挑戰,但是沒有被批准,將返回錯誤,模式匹配在這裡用於驗證聚集體的狀態,一個有限狀態機可正規化聚合根內部的狀態改變。

defmodule SegmentChallenge.Challenges.Challenge do
  @moduledoc """
  Challenges are multi-stage competitions, hosted by a club.
  Athletes compete every month during the challenge to set the fastest time for the current stage.
  """

  defstruct [
    challenge_uuid: nil,
    name: nil,
    description: nil,
    start_date: nil,
    start_date_local: nil,
    challenge_state: nil,
    # ...
  ]

  alias SegmentChallenge.Commands.{
    CreateChallenge,
    IncludeCompetitorsInChallenge,
    HostChallenge,
    StartChallenge,
    EndChallenge,
  }

  alias SegmentChallenge.Events.{
    ChallengeCreated,
    CompetitorsJoinedChallenge,
    ChallengeHosted,
    ChallengeStarted,
    ChallengeEnded,
  }

  alias SegmentChallenge.Challenges.Challenge

  @doc """
  Create a new challenge
  """
  def create_challenge(challenge, create_challenge)

  def create_challenge(%Challenge{challenge_state: nil}, %CreateChallenge{} = create_challenge) do
    %ChallengeCreated{
      challenge_uuid: create_challenge.challenge_uuid,
      name: create_challenge.name,
      description: create_challenge.description,
      # ...
    }
  end

  def create_challenge(%Challenge{}, %CreateChallenge{}), do: {:error, :challenge_already_created}

  @doc """
  Start the challenge, making it active
  """
  def start_challenge(challenge, start_challenge)

  def start_challenge(%Challenge{challenge_uuid: challenge_uuid, challenge_state: :approved} = challenge, %StartChallenge{}) do
    %ChallengeStarted{
      challenge_uuid: challenge_uuid,
      start_date: challenge.start_date,
      start_date_local: challenge.start_date_local,
    }
  end

  def start_challenge(%Challenge{}, %StartChallenge{}), do: {:error, :challenge_not_approved}

  def apply(%Challenge{} = challenge, %ChallengeCreated{challenge_uuid: challenge_uuid, name: name, description: description}) do
    %Challenge{challenge |
      challenge_uuid: challenge_uuid,
      name: name,
      description: description,
      challenge_state: :created,
      # ...
    }
  end

  def apply(%Challenge{} = challenge, %ChallengeStarted{}) do
    %Challenge{challenge |
      challenge_state: :active,
    }
  end
end
<p class="indent">


更多詳細開發步驟和說明見原文:

Building a CQRS/ES web application in Elixir using

Hacker News關於CQRS/ES的討論

[該貼被banq於2017-01-07 10:49修改過]

相關文章