基于Elixir使用Phoenix建立CQRS/ES应用

17-01-07 banq
         

该文介绍了 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>

更多详细开发步骤和说明见原文:

Building a CQRS/ES web application in Elixir using

Hacker News关于CQRS/ES的讨论

[该贴被banq于2017-01-07 10:49修改过]