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

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

更多详细开发步骤和说明见原文:
Building a CQRS/ES web application in Elixir using

Hacker News关于CQRS/ES的讨论
[该贴被banq于2017-01-07 10:49修改过]