Elixir의 상태기로 솔루션 설계

소프트웨어의 실행 흐름을 완성하기 위해 마법사, 접근 프로세스, 게임 규칙, 논리 회로 등 기능을 설계해야 할 수도 있습니다. 이 해결 방안을 구축하는 좋은 방법은 상태기라고 불리는 수학 추상에 의존해서 설계하는 것입니다.
기본적으로 상태기는 추상기의 실현으로 가능한 상태를 설명하고 상태 전환을 제어하여 정확한 집행 흐름을 확보한다.더 자세한 정의는 wikipedia에서 읽을 수 있습니다.
이 글에서 우리는 Elixir에서 이러한 것을 어떻게 실현하는지, 그리고 매개 변수에 함수 모델을 작성함으로써 if, switch 또는 case 문장을 사용하지 않은 상황에서 의도적인 성명성 설계를 해서 이런 언어에서 이를 실현하는지를 볼 수 있다.
다음 그림을 예로 들겠습니다.여기에 우리는 사이트 사용자가 등록하는 가설 절차가 있다.

다음은 프로세스입니다.
  • 사용자는 등록표를 제출해야 한다. 이것은 흐름의 시작점과 registration_form값이 표시하는 상태기의 초기 상태이다.
  • 사용자가 폼을 제출할 때 우리는 이를 form_submitted이라는 이벤트로 표시하고 이 이벤트는 다음 상태인 awaiting_email_confirmation의 전환을 촉발한다.
  • awaiting_email_confirmation에서
  • 사용자는 resend_email_confirmation 이벤트를 트리거하고 이 이벤트는 상태를 awaiting_email_confirmation으로 유지할 수 있습니다.
  • 사용자는 자신의 이메일을 확인하고 email_confirmed 이벤트를 촉발하여 상태를 registration_finished으로 변경하여 사용자 등록 절차를 완성한다.
  • 이제 이 도표를 장생불로약으로 바꾸는 방법을 살펴보자.
    우선, 우리는 User이라는 모듈을 정의할 수 있다. 이 모듈은 사용자와 관련 필드를 포함하는 구조, 그리고 state 필드를 대표하는데 이 필드는 기본 초기 상태 값이 registration_form인 상태를 저장한다(말 그대로).
    defmodule User do
      defstruct [:name, :email, state: :registration_form]
    end
    
    iex 세션에서 이 모듈을 정의하고 모듈 구조를 계산하면 다음을 반환합니다.
    iex> %User{}
    %User{
      email: nil,
      name: nil,
      state: :registration_form
    }
    
    여기에 도표의 초기 장면이 있습니다. registration_form 상태에 있는 사용자입니다.좋습니다. 하지만 우리는 어떻게 전환을 진행합니까?필요할 때만 state 필드를 업데이트합니까?필요 없습니다. 추상적인 기계를 실현하고 있습니다. 실행 흐름을 제어해야 하며, 규칙을 준수해야 한다는 것을 기억하십시오.

    전환 구현


    지금은 전환을 실현하는 부분이며, 우리는 패턴이 일치하는 신기한 장생불로약 기능에 의존한다.이 슬라이드에서는 User 모듈에 다음과 같은 API를 제공합니다.
    iex> User.transit(user, event: "form_submitted")
    {:ok, %User{state: :awaiting_email_confirmation}}
    
    첫 번째 파라미터로서 우리는 사용자 변수를 제공했다. 이것은 %User{} 구조이고 두 번째 파라미터로서 우리는 키워드 event을 제공하여 우리가 실행해야 할 조작을 나타냈다.따라서 User.transit/2 함수는 하나의 원조를 되돌려야 하고 두 번째 값은 새로운/다음 상태를 가진 사용자이다.
    내부에서, 우리의 상태기는 사용자의 현재 상태와 이벤트에 따라 이 이벤트를 허용하는지 확인해야 한다.그럼 첫 번째 도표 절차를 실현하고 그것을 봅시다.
    defmodule User do
      defstruct [:name, :email, :password, state: :registration_form]
    
      def transit(%User{state: :registration_form} = user, event: "form_submitted") do
        {:ok, %User{user | state: :awaiting_email_confirmation}}
      end
    end
    
    transit 함수에서 우리는 state의 값, 즉 두 번째 매개 변수, 즉 하나의 키워드인 event의 키워드 목록에 따라 사용자 속성 모델의 일치에 대해 분해한다.차트에 따르면 registration_form 상태이고 이벤트가 form_submitted 인 사용자만 awaiting_email_confirmation 으로 사용자를 전송합니다.만약 분명하지 않다면, 이 코드가 어떻게 초기 단계를 완성하는지 다시 한 번 도표를 보십시오.
    지금 당신은 우리가 어떻게 다음 과도를 실시해야 한다고 생각합니까?사용자가 awaiting_email_confirmation 상태에 있고 이벤트가 resend_email_confirmation인 도표에 따라 자신을 실현하려고 생각하고 시도한다.우리의 상태기는 어느 주로 옮겨야 합니까?
    defmodule User do
      defstruct [:name, :email, :password, state: :registration_form]
    
      def transit(%User{state: :registration_form} = user, event: "form_submitted") do
        {:ok, %User{user | state: :awaiting_email_confirmation}}
      end
    
      def transit(%User{state: :awaiting_email_confirmation} = user, event: "resend_email_confirmation") do
        {:ok, user}
      end
    end
    
    보시다시피 우리의 다음 전환은 우리가 원하는 조합 (상태 + 이벤트) 에 대한 패턴 일치입니다. 사용자 자체만 되돌려줍니다. 이 단계에서 우리는 같은 사용자 상태를 유지해야 하기 때문입니다.
    이제 마지막 전환: 사용자 상태 awaiting_confirmation_email, 이벤트 email_confirmed, 사용자 상태 registration_finished으로 변경
    defmodule User do
      defstruct [:name, :email, state: :registration_form]
    
      def transit(%User{state: :registration_form} = user, event: "form_submitted") do
        {:ok, %User{user | state: :awaiting_email_confirmation}}
      end
    
      def transit(%User{state: :awaiting_email_confirmation} = user, event: "resend_email_confirmation") do
        {:ok, user}
      end
    
      def transit(%User{state: :awaiting_email_confirmation} = user, event: "email_confirmed") do
        {:ok, %User{user | state: :registration_finished}}
      end
    end
    
    지금까지 우리는 거의 우리의 실현을 완성했지만 도표에 따르면 우리는 이미 기능상태기를 가지고 있다.iex 과정으로 돌아가서 다음을 시도해 봅시다.
    iex> user = %User{name: "Luke", email: "[email protected]"}
    %User{name: "Luke", email: "[email protected]", state: :registration_form}
    
    iex> {:ok, user} = User.transit(user, event: "form_submitted")
    {:ok, %User{name: "Luke", email: "[email protected]", state: :awaiting_email_confirmation}}
    
    iex> {:ok, user} = User.transit(user, event: "resend_email_confirmation")
    {:ok, %User{name: "Luke", email: "[email protected]", state: :awaiting_email_confirmation}}
    
    iex> {:ok, user} = User.transit(user, event: "email_confirmed")
    {:ok, %User{name: "Luke", email: "[email protected]", state: :registration_finished}}
    
    멋지다. 우리는 이미 모든 도표 절차를 완성했다. 그러나 만약 우리가 의외의 전환을 시도한다면 무슨 일이 일어날까?
    iex> User.transit(%User{}, event: "email_confirmed")
    ** (FunctionClauseError) no function clause matching in User.transit/2    
    
        The following arguments were given to User.transit/2:
    
            # 1
            %User{email: nil, name: nil state: :registration_form}
    
            # 2
            [event: "email_confirmed"]
    
    이것은 예상한 바와 같이 우리의 상태기가 작동하고 있습니다. 우리는 이러한 전환을 허용하지 않습니다. 이것은 registration_form 상태에 있는 사용자이기 때문에 email_confirmed 사건을 실행하고 있습니다. 이것은 분명히 등록 과정을 완성하려고 시도한 것이고 전자 우편을 확인할 필요가 없습니다.
    만약 필요하다면,catch-all 함수를 정의할 수 있습니다. 지금까지 우리가 정의한 함수가 일치하지 않으면, 이 함수는 허용되지 않는 변환 오류를 되돌려 프로그램이 붕괴되지 않도록 합니다.다음 함수를 모듈의 모든 다른 함수 다음의 마지막 함수로 정의합니다.
    defmodule User do
      # ...
      def transit(_, _), do: {:error, :transition_not_allowed}
    end
    
    따라서, User.transit/2을 호출할 때, Elixir는 정의된 순서의 모든 함수를 검사하고, 일치하지 않으면 이catch all 함수를 호출합니다.이것은 여기에서 중요하지 않기 때문에, 우리는 곧 나타날 매개 변수를 무시할 뿐입니다.다시 한 번 시도해 봅시다.
    iex> User.transit(%User{}, event: "email_confirmed")
    {:error, :transition_not_allowed}
    
    마지막으로 이것은 우리의 최종 실현이다. 이것은 우리가 소프트웨어 기능의 완전성을 확보하고 다른 부분과 분리하는 데 도움을 줄 수 있다.
    defmodule User do
      defstruct [:name, :email, state: :registration_form]
    
      def transit(%User{state: :registration_form} = user, event: "form_submitted") do
        {:ok, %User{user | state: :awaiting_email_confirmation}}
      end
    
      def transit(%User{state: :awaiting_email_confirmation} = user, event: "resend_email_confirmation") do
        {:ok, user}
      end
    
      def transit(%User{state: :awaiting_email_confirmation} = user, event: "email_confirmed") do
        {:ok, %User{user | state: :registration_finished}}
      end
    
      def transit(_, _), do: {:error, :transition_not_allowed}
    end
    

    Elixir의 상태기libs


    Elixir에는 우리가 상태기를 실현하는 데 도움을 줄 수 있는 lib도 있습니다. 그 중 하나는 내가 만든 machinist입니다. 이것은 내가 작업 중에 상태기를 작성하는 데 도움을 줄 수 있습니다.내가 그 책을 쓰기 전에 나는 당시에 다른lib이 있었다는 것을 알았지만 exto와 프로세스와 결합된 것이 필요했다. 그들은 사용하기 쉽고 좋은DSL이 있어서 읽기 쉽다. 설령 우리 팀의 비개발자라도 코드에서 규칙을 이해할 수 있고 Elixir 경험이 없는 초보 개발자도 있다.
    기계사로 위의 예를 쓰자.
    defmodule User do
      defstruct [:name, :email, state: :registration_form]
    
      use Machinist
    
      transitions do
        from :registration_form,           to: :awaiting_email_confirmation, event: "form_submitted"
        from :awaiting_email_confirmation, to: :awaiting_email_confirmation, event: "resend_email_confirmation"
        from :awaiting_email_confirmation, to: :registration_finished,       event: "email_confirmed"
      end 
    end
    
    이 코드가 생성한api는 본고에서 작성한 예시와 같다. 위의 실현은 User.transit/2 모듈에서 User 함수를 만들었다.
    위의 DSL은 대량의 템플릿 파일을 제거하여 유지 보수가 쉽고 오류가 발생하지 않도록 도와주는 방법입니다.
    다음은 제가 알고 있는 다른 상태 라이브러리입니다. 다른 라이브러리가 귀하의 질문에 더 적합할 수도 있습니다.
  • machinery

  • gen_statem(실제로는 Erlang 모듈)
  • fsmx
  • 결론


    상태기로 해결 방안을 설계하면 우리의 지원을 받고 부패한 상태가 없도록 하며, 우리의 소프트웨어가 의외의 행위가 발생하지 않도록 확보할 수 있다.우리의 소프트웨어에서 큰 유량을 유지하고 상태기가 없으면 오류에 노출되기 쉽다고 상상해 보세요.그 밖에 Elixir에서 그것을 실현하려면, 우리는 의도적이고 성명적인 코드와 업무 논리 흐름이 하나 더 있는데, 이것은 전체 국면을 이해하기 쉽다.
    네가 그것을 좋아하길 바란다. 아마도 다음에 네가 문제를 해결해야 할 때 상태기를 사용하면 너를 도울 수 있을 것이다.

    좋은 웹페이지 즐겨찾기