Введение в gen_fsm: Банкомат Erlybank

Автор оригинала: Mitchell Hashimoto
  • Перевод
Предыстория
Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
Введение в gen_server: «Erlybank»

Это вторая статья из серии «Введение в OTP». Рекомендую вам прочитать первую статью, в которой говорится о gen_server и закладываются основы нашей банковской системы перед тем, как читать эту. Если вы быстро схватываете, можете посмотреть завершенную версию сервера и двигаться дальше.

Сценарий: Мы поставили ErlyBank сервер заказчикам, и они остались очень довольны. Но на дворе 21 век, и они хотят также безопасный и простой в использовании банкомат, поэтому они попросили нас расширить наш сервер и создать программное обеспечение для банкомата. Пользовательские аккаунты должны быть защищены 4-цифровым ПИН-кодом. В банкомате можно залогиниться с помощью ранее созданного аккаунта, сделать депозит или снять деньги со счета. Делать красивый интерфейс не требуется, этим занимаются другие люди.

Цель: Сначала мы расширим сервер, добавив поддержку ПИН-кода для аккаунтов и авторизации через ПИН-код. Потом мы будем использовать gen_fsm чтобы создать бэкенд банкомата. Проверка данных будет проводиться на стороне сервера.

Что такое gen_fsm?


gen_fsm — это еще один Erlang/OTP интерфейсный модуль. Он используется для реализации конечного автомата.

Я заранее извиняюсь, так как в этой статье понятие «состояние»(state) будет использоваться для обозначения двух вещей:
  • состояние gen_fsm — Состояние конечного автомата, текущий «режим» его работы. Оно не имеет ничего общего с состоянием(данными) из gen_server.
  • состояние(данные) — Данные состояния сервера — то, о чем вы узнали из предыдущей статьи про gen_server.

Немного не ловко, конечно, но я постараюсь ссылаться на них только в контексте вышеобозначенных условий.

gen_fsm начинает работу в некотором состоянии. Любые call/cast вызовы к gen_fsm обрабатываются в специальных callback-методах, которые должны называться именем текущего состояния gen_fsm(конечного автомата). Основываясь на произведенном действии, модуль может менять состояние( [текущее состояние, входящий символ] -> новое состояние, прим.). Хрестоматийный пример конечного автомата — это закрытая дверь. В начале дверь находится в состоянии «закрыта». Необходимо ввести 4-значный код, чтобы открыть ее. После ввода 1 цифры дверь сохраняет ее, но одной цифры мало, поэтому она продолжает ждать в состоянии «закрыта». После ввода 4 цифр, если они правильные, дверь меняет состояние на «открыта» на некоторое время. Если цифры не правильные, она остается в состоянии «закрыта» и очищает память. Возможно, сейчас у вас уже появились догадки о том, как мы будем реализовывать конечный автомат с помощью gen_fsm:)

Так же, как и в случае с gen_server, представляю список callback-методов, которые должны быть реализованы в gen_fsm. Вы найдете много общего с gen_server:
  • init/1 — Инициализирует сервер конечного автомата. Практически идентичен gen_server.
  • StateName/2 — StateName будет заменено на название состояния. Этот метод вызывается, когда конечный автомат находится в этом состоянии и получает сообщение. В результате, выполняется определенное действие. Это асинхронный callback-метод.
  • handle_event/3 — То же, что и StateName/2, за исключением того, что этот метод срабатывает, когда клиент вызывает gen_fsm:send_all_state_event, независимо от текущего состояния автомата. Опять, же асинхронный.
  • StateName/3Синхронная версия StateName/2. Клиент дожидается ответа сервера.
  • handle_sync_event/4 — Синхронная версия handle_event/3.
  • handle_info/3 — Эквивалентно gen_server handle_info. Этотм метод получает все сообщения, которые были посланы нестандартными средствами gen_fsm. Это могут быть таймаут-сообщения, process exit сообщения или любые другие сообщения, посланные серверному процессу с помощью "!".
  • terminate/3 — Вызывается, когда сервер завершает работу, в нем вы можете освободить занятые ресурсы.
  • code_change/4 — Вызывается, когда сервер обновляется в реальном времени. Мы не используем его сейчас, но он будет использоваться в будущих статьях.

gen_fsm скелет


Так же, как и с gen_server, я начинаю создавать конечный автомат с некоторого общего скелета. Скелет для gen_fsm можете найти здесь.

Там нет ничего экстраординарного. start_link похож на тот, что мы создали для gen_server. :) Сохраните скелет, как eb_atm.erl. И вот, мы готовы начать!

Расширение eb_server для создания механизма авторизации аккаунта.


Это еще одно задание, которое, я оставляю вам. Изменения, которые нам необходимы:

  1. Теперь при создании аккаунта необходимо требовать ПИН-код, который будет храниться вместе с аккаунтом, без шифрования.
  2. Добавить метод authorize/2 с аргументами Name и PIN. Возвращаемые значения должны быть ok или {error, Reason}.

Также, было бы замечательно требовать ПИН-код при каждой операции депозит/снятие, но, чтобы сэкономить время, а также из-за того, что банк у нас фейковый(мое сердце разбито:( ха!), мы не будем этого делать.

По-честному, это не так-то просто сделать, но если вы учите Erlang сами, вы должны быть достаточно смышленными ;) Так что я думаю, вы можете сделать это! Протестируйте свои изменения перед тем, как продолжать или, по крайней мере, сравните их с ответом внизу.

После внесения изменений ваш eb_server.erl должен выглядеть примерно так. Обратите внимание, что сообщения, которые вы посылаете серверу могут быть разными, и это нормально. Мышление у всех разное. Очень важно, чтобы API выводил те же данные, правильно. (The important thing is that the API outputs the same data, correctly, англ.)

Стратегия проектирования ATM (ATM Design Strategy)


Я хочу взять маленькую «без кода» паузу, чтобы рассказать план работы конечного автомата ATM. Мы собираемся его выполнить в соответствии с диаграммой ниже:

Sequence Diagram for ATM

Три голубых блока представляют разные состояния сервера. Стрелки обозначают, какие действия необходимы, чтобы перейти из одного состояния в другое.

Инициализация gen_fsm


Чтобы Запустить ATM, мы используем такой же метод start_link, как и в gen_server. Но инициализация немного отличается.
init([]) ->
  {ok, unauthorized, nobody}. 

Метод init/1 модуля gen_fsm должен возвращать {ok, StateName, StateData}. StateName — это начальное состояние сервера, а StateData — это начальное состояние(данные) сервера. В нашем случае мы запускаемся в состоянии unauthorized и данные выставляются в nobody. Состоянием(данными) будет имя аккаунта, с которым мы работаем, так что сначала там ничего нет. В Erlang нет типа данных null/nil/nothing, вместо которых обычно используется говорящий атом, как у нас nobody, например.

Авторизация аккаунта


Теперь нам нужно реализовать API авторизации для ATM. Сначала, определение API:

authorize(Name, PIN) ->
  gen_fsm:sync_send_event(?SERVER, {authorize, Name, PIN}).

Метод sync_send_event эквивалентен методу call модуля gen_server. Он посылает сообщение(вторым аргументом) текущему состоянию сервера(первый аргумент). Поэтому теперь нам надо написать обработчик этого сообщения:
unauthorized({authorize, Name, Pin}, _From, State) ->
  case eb_server:authorize(Name, Pin) of
    ok ->
      {reply, ok, authorized, Name};
    {error, Reason} ->
      {reply, {error, Reason}, unauthorized, State}
  end;
unauthorized(_Event, _From, State) ->
  Reply = {error, invalid_message},
  {reply, Reply, unauthorized, State}.

Функция называется unauthorized потому, что она должна получать сообщение, когда сервер находится в состоянии unauthorized. Делаем сопоставление с образцом, чтобы обработать тапл {authorize, Name, Pin} и используем API методы, экспортированные eb_server сервером, чтобы авторизировать пользователя.

Если имя пользователя и ПИН-код правильные, мы посылаем ok клиенту. Формат ответа: {reply, Response, NewStateName, NewStateData}. В соответствии с форматом, мы изменяем состояние в authorized и сохраняем имя аккаунта в состоянии(данных).

Если информация по аккаунту была не верной, мы посылаем в ответ error и причину ошибки, состояние и состояние(данные) при этом не меняются.

В конце мы реализуем еще одну «catch-all» функцию. Вам следует делать так всегда, но здесь это особенно важно, так как состояния могут получать сообщения, адресованные другим состояниям. Например: что будет, если по какой-то причине кто-либо попытается сделать депозит в неаторизованном состоянии? Нам нужен «catch-all» метод, чтобы послать обратно сообщение об ошибке.

Депозит


Как только мы перешли в авторизованное состояние, пользователь собирается сделать депозит или снять деньги со своего банковского аккаунта. Мы реализуем депозит, используя асинхронный вызов к серверу. Опять же, это не очень безопасно: мы вообще не проверяем, был ли депозит успешным, но так как наш банк — это фейк, я на это забью. ;)

Итак, в начале, API!
%%--------------------------------------------------------------------
%% Function: deposit(Amount) -> ok
%% Description: Deposits a certain amount in the currently authorized
%% account.
%%--------------------------------------------------------------------
deposit(Amount) ->
  gen_fsm:send_event(?SERVER, {deposit, Amount}).

Все просто, в этот раз мы используем метод send_event/2 вместо sync_send_event. Он посылает асинхронный вызов серверу. А теперь, обработчик…
authorized({deposit, Amount}, State) ->
  eb_server:deposit(State, Amount),
  {next_state, thank_you, State, 5000};
authorized(_Event, State) ->
  {next_state, authorized, State}.

Опять, все очень просто. Этот метод просто перенаправляет информацию в метод deposit модуля eb_server, который также проводит всю проверку. Но есть что-то необычное в возвращаемом значении метода deposit! Не только состояние меняется на thank_you, но еще и это число «5000» там в конце. Это просто таймаут. Если не будет получено никакого сообщения в течение 5000 милисекунд(5 секунд), то текущему состоянию пошлется таймаут-сообщение.
Которе ведет нас к следующей теме…

Короткое «Thank You!» состояние


Многие(или все), кто пользовался банкоматами, знают, что есть такой маленький «Thank You!» экран, который показывается в течение небольшого времени. Вообще-то мы могли спокойно обойтись и без этого экрана в нашей реализации — я просто хотел показать вам фичу с таймаутом в gen_fsm. После 5000 милисекунд, или если не будет получено никакого сообщения, я меняю состояние на обратно на «unauthorized», и таким образом, ATM может начать работу заново со следующим пользователем. Вот код:
thank_you(timeout, _State) ->
  {next_state, unauthorized, nobody};
thank_you(_Event, _State) ->
  {next_state, unauthorized, nobody}.

Примечание: Тренированный глаз заметит, что оба метода эквивалентны, и нет необходимости для первого образца. Это правда, я просто включил первый образец, чтобы быть уверенным, что поймаю таумаут.

А здесь законченная на данный момент версия eb_atm.erl.

Изъятие денег со счета


Снова я оставлю разработку методов для снятия денег как упражнение читателю. Вы можете реализовать эту задачку, как хотите! Просто удостоверьтесь, что ваши реально снимают деньги ;)

Вот моя версия eb_atm.erl после реализации механизмов снятия денег со счета. Обратите внимание, что успешная операция перевадит автомат в состояние thank_you с таймаутом.

«Отменить-по-фигу-че»(«Cancel-No-Matter-What») Кнопка


Одна из самых больших проблем компьютеров — это отсутствие кнопки «Отменить», которая прерывала бы все, что вы делаете. И хотя я знаю, что кнопка power off на компьютере справляется с этой задачей на ура, пользователи банкоматов Erlybank лишены такой возможности. Поэтому давайте реализуем cancel метод, который отменял бы все транзакции, вне зависимости от того, в каком вы состоянии находитесь.

Как бы вы реализовали это? В общем, я бы предположил, что вы, основываясь на информации из этой статьи, сделали бы cancel метод, посылающий сообщение cancel. Затем, в каждом состоянии вы бы обработали его и вышли бы обратно в unauthorized состояние.

Остроумно, но не правильно, но в этом нет вашей вины! Я не указал (или слишком кратко, вы возможно, пропустили это), что есть метод gen_fsm:send_all_state_event/2, который послылает сообщение серверу вне зависимости от того, в каком состоянии сервер. Мы используем его, чтобы наш код был чист.

Наш API:
%%--------------------------------------------------------------------
%% Function: cancel/0
%% Description: Cancels the ATM transaction no matter what state.
%%--------------------------------------------------------------------
cancel() ->
  gen_fsm:send_all_state_event(?SERVER, cancel).

Это сообщение посылается handle_event/3, который мы расширяем ниже:
handle_event(cancel, _StateName, _State) ->
  {next_state, unauthorized, nobody};
handle_event(_Event, StateName, State) ->
  {next_state, StateName, State}.

Если мы получаем cancel сообщение, сервер переводит состояние в unauthorized и сосяние(данные) в nobody: свеженький ATM!

Как всегда, текущую версию eb_atm.erl можно посмотреть тут.

Заключительные примечания


В этой статье я показал, как создать простую ATM систему, построенную на конечном автомате, используя gen_fsm. Я показал, как обрабатывать сообщения в разных состояниях, менять состояние, менять состояние по таймауту, и «send-to-all» сообщения.

Однако, есть еще немного «бородавок» в нашей системе, и я оставлю вам возможность исправить их. Я подготовил 2 задания для вас, если хотите. Поверьте мне, вы можете их выполнить:
  1. Добавить проверку ошибок в операции с депозитом. Заставьте их возвращать {error, Reason} и {ok, Balance} вместо просто «ok» все время.
  2. Добавьте функцию проверки баланса в ATM. Она должна быть доступна только в состоянии authorized и не должна завершать транзакцию. Это означает, что она не должна переводить состояние в thank_you. Это так потому, что обычно люди, помотрев свой баланс, хотят снять или положить денег себе на счет.

Эти две фичи из упражнений не будут использоваться в будущем, и раз так, я не буду постить ответы здесь. Вы можете проверить себя, заставив их работать! :)

Вторая часть этих статей закончена. Третья статья уже почти готова и будет опубликована в ближайшие дни. Она будет раскрывать тему gen_event. Чтобы поразвлечься, вы можете подумать о том, что я добавлю в Erlybank с помощью gen_event! :D

Надеюсь, вам понравились эти вводные статьи в Erlang/OTP так же, как и мне понравилось их писать. Спасибо всем за поддержку и удачи!

Комментарии 4

    0
    А продолжение планируется?
      0
      Да, скоро.
      0
      А где продолжение?
        0
        Когда закончилась карма, тогда и закончились переводы ))

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое