[Elixir/Phoenix] Создаем конечный автомат в Elixir и Ecto
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Существует много полезных шаблонов проектирования и концепция конечного автомата входит в число полезных шаблонов проектирования.
Конечный автомат отлично подходит в случаях, когда когда вы моделируете сложный бизнес-процесс, в котором происходит переход состояний из предопределенного набора состояний и каждое состояние должно обладать своим предопределенным поведением.
В этой публикации вы узнаете, как реализовать этот шаблон с помощью Elixir и Ecto.
Случаи использования
Конечный автомат может быть отличным выбором когда вы моделируете сложный, многошаговый бизнес-процесс, и где к каждому шагу предъявляются определенные требования.
Примеры:
- Регистрация в личном кабинете. В этом процессе пользователь сначала регистрируется, потом добавляет некоторую дополнительную информацию, затем подтверждает свою электронную почту, затем включает 2FA, и только после этого получает доступ в систему.
- Корзина для покупок. Сперва она пустая, потом в неё можно добавить товары и после чего пользователь может перейти к оплате и доставке.
- Конвейер задач в системах управления проектами. Например: изначально задачи в статусе "создана", потом задача может быть "назначена" исполнителю, затем статус изменится на "в процессе", а затем в "выполнено".
Пример конечного автомата
Приведем небольшой учебный пример, иллюстрирующий работу конечного автомата: работа двери.
Дверь может быть заблокирована или разблокирована. Она также может быть открыта или закрыта. Если она разблокирована, то её можно открыть.
Мы можем смоделировать это как конечный автомат:
Этот конечный автомат имеет:
- 3 возможных состояния: заблокирована, разблокирована, открыта
- 4 возможных перехода состояния: разблокировать, открыть, закрыть, заблокировать
Из диаграммы можно сделать вывод, что невозможно перейти от заблокирована к открыта. Или простыми словами: сначала нужно разблокировать дверь, а уже потом открыть. Данная диаграмма описывает поведение, но как реализовать это?
Конечные автоматы как Elixir процессы
Начиная с OTP 19, Erlang предоставляет модуль :gen_statem, который позволяет реализовывать процессы, подобные gen_server, которые ведут себя как конечные автоматы (в которых текущее состояние влияет на их внутреннее поведение). Давайте посмотрим, как это будет выглядеть для нашего примера с дверью:
defmodule Door do
@behaviour :gen_statem
# Стартуем сервис
def start_link do
:gen_statem.start_link(__MODULE__, :ok, [])
end
# начальное состояние, вызываемое при старте, locked - заблокировано
@impl :gen_statem
def init(_), do: {:ok, :locked, nil}
@impl :gen_statem
def callback_mode, do: :handle_event_function
# обработка приходящего события: разблокируем заблокированную дверь
# next_state - новое состояние - дверь разблокирована
@impl :gen_statem
def handle_event({:call, from}, :unlock, :locked, data) do
{:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]}
end
# блокировка разблокированной двери
def handle_event({:call, from}, :lock, :unlocked, data) do
{:next_state, :locked, data, [{:reply, from, {:ok, :locked}}]}
end
# открытие разблокированной двери
def handle_event({:call, from}, :open, :unlocked, data) do
{:next_state, :opened, data, [{:reply, from, {:ok, :opened}}]}
end
# закрытие открытой двери
def handle_event({:call, from}, :close, :opened, data) do
{:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]}
end
# возвращение ошибки при неопределеном поведении
def handle_event({:call, from}, _event, _content, data) do
{:keep_state, data, [{:reply, from, {:error, "invalid transition"}}]}
end
end
Этот процесс начинается в состоянии :locked (заблокировано). Отправляя соответствующие события, мы можем сопоставить текущее состояние с запрошенным переходом и выполнить необходимые преобразования. Дополнительный аргумент data сохраняется для любого другого дополнительного состояния, но в этом примере мы его не используем.
Мы можем вызвать его с нужным нам переходом состояния. Если текущее состояние позволяет этот переход, то он отработает. В противном случае будет возвращена ошибка (из-за последнего обработчика события, которое отлавливает всё, что не соответствует допустимым событиям).
{:ok, pid} = Door.start_link()
:gen_statem.call(pid, :unlock)
# {:ok, :unlocked}
:gen_statem.call(pid, :open)
# {:ok, :opened}
:gen_statem.call(pid, :close)
# {:ok, :closed}
:gen_statem.call(pid, :lock)
# {:ok, :locked}
:gen_statem.call(pid, :open)
# {:error, "invalid transition"}
Если наш конечный автомат в большей степени ориентирован на данные, чем на процессы, то мы можем использовать другой подход.
Конечные автоматы как Ecto модели
Есть несколько пакетов Elixir, которые решают эту проблему. В этом посте я буду использовать Fsmx, но другие пакеты, например, Machinery, также предоставляют аналогичные функции.
Этот пакет позволяет нам моделировать точно такие же состояния и переходы, но в существующей модели Ecto:
defmodule PersistedDoor do
use Ecto.Schema
schema "doors" do
field(:state, :string, default: "locked")
field(:terms_and_conditions, :boolean)
end
use Fsmx.Struct,
transitions: %{
"locked" => "unlocked",
"unlocked" => ["locked", "opened"],
"opened" => "unlocked"
}
end
Как мы увидеть, Fsmx.Struct получает все возможные переходы в качестве аргумента. Это позволяет ему проверять нежелательные переходы и предотвращать их возникновение. Теперь мы можем изменить состояние, используя традиционный, не-Ecto подход:
door = %PersistedDoor{state: "locked"}
Fsmx.transition(door, "unlocked")
# {:ok, %PersistedDoor{state: "unlocked", color: nil}}
Но мы можем также попросить то же самое в форме Ecto changeset (используемое в Elixir слово, означающее “набор изменений”):
door = PersistedDoor |> Repo.one()
Fsmx.transition_changeset(door, "unlocked")
|> Repo.update()
Этот changeset только обновляет поле :state. Но мы можем расширить его, чтобы включить дополнительные поля и проверки. Допустим, чтобы открыть дверь, нам нужно принять ее условия:
defmodule PersistedDoor do
# ...
def transition(changeset, _from, "opened", params) do
changeset
|> cast(params, [:terms_and_conditions])
|> validate_acceptance(:terms_and_conditions)
end
end
Fsmx ищет в вашей схеме необязательную функцию transition_changeset/4 и вызывает ее как с предыдущим состоянием, так и со следующим. Вы можете сопоставить их по шаблону, чтобы добавить определенные условия для каждого переход.
Работа с побочными эффектами
Перевести конечный автомат из одного состояния в другое это общая задача для конечных автоматов. Но Еще одним большим преимуществом конечных автоматов является способность справляться с побочными эффектами, которые могут иметь место в каждом состоянии.
Допустим, мы хотим получать уведомления каждый раз, когда кто-то открывает нашу дверь. Возможно, мы захотим отправить электронную почту, когда это произойдет. Но мы хотим, чтобы эти две операции были одной атомарной операцией.
Ecto работает с атомарностью через пакет Ecto.Multi, которая группирует несколько операций внутри транзакции базы данных. Ecto также имеет функцию Ecto.Multi.run/3, которая позволяет запускать произвольный код в рамках той же транзакции.
Fsmx, в свою очередь, интегрируется с Ecto.Multi, предоставляя вам возможность выполнять переходы состояний как часть Ecto.Multi, а также предоставляет дополнительный обратный вызов, который выполняется в этом случае:
defmodule PersistedDoor do
# ...
def after_transaction_multi(changeset, _from, "unlocked", params) do
Emails.door_unlocked()
|> Mailer.deliver_later()
end
end
Теперь вы можете выполнить переход как показано:
door = PersistedDoor |> Repo.one()
Ecto.Multi.new()
|> Fsmx.transition_multi(schema, "transition-id", "unlocked")
|> Repo.transaction()
Эта транзакция будет использовать тот же transition_changeset/4, как было описано выше, для вычисления необходимых изменений в Ecto модели. И будет включать новый обратный вызов в качестве вызова Ecto.Multi.run. В результате электронное письмо отправляется (асинхронно, с использованием Bamboo, чтобы не запускаться внутри самой транзакции).
Если changeset (набор изменений) по какой-либо причине признан недействительным, электронное письмо никогда не будет отправлено, в результате атомарного выполнения обеих операций.
Заключение
В следующий раз, когда вы моделируете какое-то поведение с состоянием, подумайте о подходе с использование шаблона конечного автомата (машины конечных состояний), для вас этот шаблон может стать хорошим помощником. Он одновременно и прост и эффективен. Этот шаблон позволит смоделированную диаграмму переходов состояния легко выразить в виде программного кода, что ускорит разработку.
Сделаю оговорку, возможно акторная модель способствует простоте реализации конечного автомата в Elixir\Erlang, каждый актор имеет своё состояние и очередь входящих сообщений, которые последовательно изменяют его состояние. В книге “Проектирование масштабируемых систем в Erlang/ОТР” про конечные автоматы очень хорошо написано, в разрезе акторной модели.
Если у вас есть собственные примеры реализации конечных автоматов на вашем языке программирования, то прошу поделиться ссылкой, будет интересно изучить.
===========
Источник:
habr.com
===========
Похожие новости:
- [D, Высокая производительность] Go Your Own Way. Часть вторая. Куча (перевод)
- [Elixir/Phoenix, Erlang/OTP, Open source, Распределённые системы] Vela → умный кеш для time series и не только
- [D, Высокая производительность] Go Your Own Way. Часть первая. Стек (перевод)
- [D, Высокая производительность] Life in the Fast Lane (перевод)
- [Конференции, Elixir/Phoenix, Функциональное программирование] Анонс онлайн митапа русскоязычного Elixir community
- [D, Высокая производительность] Don’t Fear the Reaper (перевод)
- [.NET, C#, Программирование] Самые простые конечные автоматы или стейт-машины в три шага
- [Elixir/Phoenix, Erlang/OTP, Биографии гиков, Программирование] Джозеф Лесли Армстронг → Цитаты из выступлений (перевод)
- [CGI (графика), Работа с 3D-графикой] Разница между фальшивыми и истинными смещениями в 3D-графике (перевод)
- [Информационная безопасность, IT-инфраструктура, Удалённая работа] Remote Desktop глазами атакующего
Теги для поиска: #_elixir/phoenix, #_konechnyj_avtomat (конечный автомат), #_finite_state_machine, #_ecto, #_elixir, #_elixir/phoenix
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:33
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Существует много полезных шаблонов проектирования и концепция конечного автомата входит в число полезных шаблонов проектирования. Конечный автомат отлично подходит в случаях, когда когда вы моделируете сложный бизнес-процесс, в котором происходит переход состояний из предопределенного набора состояний и каждое состояние должно обладать своим предопределенным поведением. В этой публикации вы узнаете, как реализовать этот шаблон с помощью Elixir и Ecto. Случаи использования Конечный автомат может быть отличным выбором когда вы моделируете сложный, многошаговый бизнес-процесс, и где к каждому шагу предъявляются определенные требования. Примеры:
Пример конечного автомата Приведем небольшой учебный пример, иллюстрирующий работу конечного автомата: работа двери. Дверь может быть заблокирована или разблокирована. Она также может быть открыта или закрыта. Если она разблокирована, то её можно открыть. Мы можем смоделировать это как конечный автомат: Этот конечный автомат имеет:
Из диаграммы можно сделать вывод, что невозможно перейти от заблокирована к открыта. Или простыми словами: сначала нужно разблокировать дверь, а уже потом открыть. Данная диаграмма описывает поведение, но как реализовать это? Конечные автоматы как Elixir процессы Начиная с OTP 19, Erlang предоставляет модуль :gen_statem, который позволяет реализовывать процессы, подобные gen_server, которые ведут себя как конечные автоматы (в которых текущее состояние влияет на их внутреннее поведение). Давайте посмотрим, как это будет выглядеть для нашего примера с дверью: defmodule Door do
@behaviour :gen_statem # Стартуем сервис def start_link do :gen_statem.start_link(__MODULE__, :ok, []) end # начальное состояние, вызываемое при старте, locked - заблокировано @impl :gen_statem def init(_), do: {:ok, :locked, nil} @impl :gen_statem def callback_mode, do: :handle_event_function # обработка приходящего события: разблокируем заблокированную дверь # next_state - новое состояние - дверь разблокирована @impl :gen_statem def handle_event({:call, from}, :unlock, :locked, data) do {:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]} end # блокировка разблокированной двери def handle_event({:call, from}, :lock, :unlocked, data) do {:next_state, :locked, data, [{:reply, from, {:ok, :locked}}]} end # открытие разблокированной двери def handle_event({:call, from}, :open, :unlocked, data) do {:next_state, :opened, data, [{:reply, from, {:ok, :opened}}]} end # закрытие открытой двери def handle_event({:call, from}, :close, :opened, data) do {:next_state, :unlocked, data, [{:reply, from, {:ok, :unlocked}}]} end # возвращение ошибки при неопределеном поведении def handle_event({:call, from}, _event, _content, data) do {:keep_state, data, [{:reply, from, {:error, "invalid transition"}}]} end end Этот процесс начинается в состоянии :locked (заблокировано). Отправляя соответствующие события, мы можем сопоставить текущее состояние с запрошенным переходом и выполнить необходимые преобразования. Дополнительный аргумент data сохраняется для любого другого дополнительного состояния, но в этом примере мы его не используем. Мы можем вызвать его с нужным нам переходом состояния. Если текущее состояние позволяет этот переход, то он отработает. В противном случае будет возвращена ошибка (из-за последнего обработчика события, которое отлавливает всё, что не соответствует допустимым событиям). {:ok, pid} = Door.start_link()
:gen_statem.call(pid, :unlock) # {:ok, :unlocked} :gen_statem.call(pid, :open) # {:ok, :opened} :gen_statem.call(pid, :close) # {:ok, :closed} :gen_statem.call(pid, :lock) # {:ok, :locked} :gen_statem.call(pid, :open) # {:error, "invalid transition"} Если наш конечный автомат в большей степени ориентирован на данные, чем на процессы, то мы можем использовать другой подход. Конечные автоматы как Ecto модели Есть несколько пакетов Elixir, которые решают эту проблему. В этом посте я буду использовать Fsmx, но другие пакеты, например, Machinery, также предоставляют аналогичные функции. Этот пакет позволяет нам моделировать точно такие же состояния и переходы, но в существующей модели Ecto: defmodule PersistedDoor do
use Ecto.Schema schema "doors" do field(:state, :string, default: "locked") field(:terms_and_conditions, :boolean) end use Fsmx.Struct, transitions: %{ "locked" => "unlocked", "unlocked" => ["locked", "opened"], "opened" => "unlocked" } end Как мы увидеть, Fsmx.Struct получает все возможные переходы в качестве аргумента. Это позволяет ему проверять нежелательные переходы и предотвращать их возникновение. Теперь мы можем изменить состояние, используя традиционный, не-Ecto подход: door = %PersistedDoor{state: "locked"}
Fsmx.transition(door, "unlocked") # {:ok, %PersistedDoor{state: "unlocked", color: nil}} Но мы можем также попросить то же самое в форме Ecto changeset (используемое в Elixir слово, означающее “набор изменений”): door = PersistedDoor |> Repo.one()
Fsmx.transition_changeset(door, "unlocked") |> Repo.update() Этот changeset только обновляет поле :state. Но мы можем расширить его, чтобы включить дополнительные поля и проверки. Допустим, чтобы открыть дверь, нам нужно принять ее условия: defmodule PersistedDoor do
# ... def transition(changeset, _from, "opened", params) do changeset |> cast(params, [:terms_and_conditions]) |> validate_acceptance(:terms_and_conditions) end end Fsmx ищет в вашей схеме необязательную функцию transition_changeset/4 и вызывает ее как с предыдущим состоянием, так и со следующим. Вы можете сопоставить их по шаблону, чтобы добавить определенные условия для каждого переход. Работа с побочными эффектами Перевести конечный автомат из одного состояния в другое это общая задача для конечных автоматов. Но Еще одним большим преимуществом конечных автоматов является способность справляться с побочными эффектами, которые могут иметь место в каждом состоянии. Допустим, мы хотим получать уведомления каждый раз, когда кто-то открывает нашу дверь. Возможно, мы захотим отправить электронную почту, когда это произойдет. Но мы хотим, чтобы эти две операции были одной атомарной операцией. Ecto работает с атомарностью через пакет Ecto.Multi, которая группирует несколько операций внутри транзакции базы данных. Ecto также имеет функцию Ecto.Multi.run/3, которая позволяет запускать произвольный код в рамках той же транзакции. Fsmx, в свою очередь, интегрируется с Ecto.Multi, предоставляя вам возможность выполнять переходы состояний как часть Ecto.Multi, а также предоставляет дополнительный обратный вызов, который выполняется в этом случае: defmodule PersistedDoor do
# ... def after_transaction_multi(changeset, _from, "unlocked", params) do Emails.door_unlocked() |> Mailer.deliver_later() end end Теперь вы можете выполнить переход как показано: door = PersistedDoor |> Repo.one()
Ecto.Multi.new() |> Fsmx.transition_multi(schema, "transition-id", "unlocked") |> Repo.transaction() Эта транзакция будет использовать тот же transition_changeset/4, как было описано выше, для вычисления необходимых изменений в Ecto модели. И будет включать новый обратный вызов в качестве вызова Ecto.Multi.run. В результате электронное письмо отправляется (асинхронно, с использованием Bamboo, чтобы не запускаться внутри самой транзакции). Если changeset (набор изменений) по какой-либо причине признан недействительным, электронное письмо никогда не будет отправлено, в результате атомарного выполнения обеих операций. Заключение В следующий раз, когда вы моделируете какое-то поведение с состоянием, подумайте о подходе с использование шаблона конечного автомата (машины конечных состояний), для вас этот шаблон может стать хорошим помощником. Он одновременно и прост и эффективен. Этот шаблон позволит смоделированную диаграмму переходов состояния легко выразить в виде программного кода, что ускорит разработку. Сделаю оговорку, возможно акторная модель способствует простоте реализации конечного автомата в Elixir\Erlang, каждый актор имеет своё состояние и очередь входящих сообщений, которые последовательно изменяют его состояние. В книге “Проектирование масштабируемых систем в Erlang/ОТР” про конечные автоматы очень хорошо написано, в разрезе акторной модели. Если у вас есть собственные примеры реализации конечных автоматов на вашем языке программирования, то прошу поделиться ссылкой, будет интересно изучить. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:33
Часовой пояс: UTC + 5