Elixir의 프로토콜 대 행동: 추가 고려 사항

초기 생각



최근에 나는 Elixir의 Protocols vs. Behaviors에 대한 환상적인article by Yiming Chen을 읽었습니다.

나는 훌륭한 기사에 대해 저자에게 감사하고 싶습니다. 이 기사는 많은 것을 명확하게 하고 제가 혼자서는 얻을 수 없는 몇 가지 아이디어를 엄격하게 공식화합니다.

여기에 Protocols vs. Behaviours의 사용 사례에 대한 몇 가지 생각을 추가하고 싶습니다. 정확히 왜 우리는 여전히 Protocols 대신 Behaviors를 자주 사용합니다.

제 개인적인 비공식 요지는 다음과 같습니다.

Protocols are suitable for pure interfaces (as stated in the article) of pure data structures.



프로세스와 메시지 전송과 같은 부작용을 다루는 프로토콜을 사용하려고 할 때 종종 어색함을 느끼기 시작합니다.

그 이유는 부작용이 있는 것은 OTP 조각( GenServer )으로 빌드되고 다음과 같습니다.
  • 우리는 원래 디자인에 따라 편안함을 느낍니다.
  • 지연된 초기화는 필수적이며 프로토콜에서는 잘 보이지 않습니다.

  • 삽화



    그것을 설명하겠습니다.

    나는 작은 라이브러리SMPPEX를 개발하고 거기에 Session 라는 모듈과 동작이 있습니다. 감싸고 확장하는 것만 중요합니다GenServer, 즉
  • init , handle_call , ... 및 일부 자체 콜백(handle_pdu 등)과 같은 콜백을 지정합니다. 일반적으로 작동합니다. init 일부 상태를 초기화한 다음 콜백.
  • Session.start_link , Session.call , Session.cast , Session.send_pdu 등과 같은 인터페이스 기능을 구현합니다.

  • 순진한 디자인



    이 모듈을 처음 구현하려고 했을 때 프로토콜이라는 아이디어를 적용하려고 했습니다.

    콜백을 지정하는 SessionState 와 같은 프로토콜을 만들었습니다.

    defprotocol SessionState do
      def handle_call(st, from, message)
      def handle_pdu(st, pdu)
      ...
    end
    

    Session 를 사용하려면 다음을 수행합니다.

    먼저 SessionState 를 구현합니다.

    defmodule SessionStateImpl do
      defstruct [...]
    
      def new(args) do
        ...
      end
    end
    
    defimpl SessionState, for: SessionStateImpl do
      def handle_pdu(st, pdu) do
        ...
        {:noreply, new_st}
      end
    end
    


    그런 다음 인스턴스를 만들고 Session 에 전달합니다.

    st = SessionStateImpl.new(args)
    {:ok, pid} = Session.start_link(st, ...)
    


    얼핏 보기에는 모든 것이 훌륭해 보이지만 PoC 앱을 만들다가 문제에 봉착했습니다. OTP에서 초기화 컨텍스트가 필수적이라는 것입니다.
    GenServers 를 구현할 때 사용자는 종종 다음과 같은 작업을 하고 싶어합니다.

    def init(opts) do
      ...
      timer = :erlang.start_timer(@interval, self(), :tick)
      ...
    end
    


    타이머를 설정하거나 전역 등록 등과 같은 다른 유용한 작업을 수행하려면 세션의 PID를 알아야 합니다.

    하지만 SessionStateImpl 시작하기 전에 SessionStateImpl.new (call Session )를 생성하고 그렇게 할 장소가 없습니다.

    가능한 개선 사항



    솔루션 1


    SessionStateImpl 내부가 그렇게 할 수 있도록 Session 생성 및 초기화를 연기할 수 있습니다.

    # st = SessionStateImpl.new(args)
    {:ok, pid} = Session.start_link(SessionStateImpl, :new, args, ...)
    


    그러나 우리가 Protocol을 사용하고 모듈과 상태를 전달하지 않고 다시 전달하기 시작했다고 생각하지 않습니다. :)

    솔루션 2



    초기화를 연기하고 SessionState 프로토콜의 일부로 만들 수 있습니다. 문제는 기본 구조체 없이 Protocol을 사용할 수 없다는 것이므로 다음과 같이 해야 합니다.

    defprotocol SessionState do
      ...
      def init(uninitialized_st, args)
      ...
    end
    
    ...
    
    defimpl SessionState, for: SessionStateImpl do
      def init(uninitialized_st, args) do
        ...
        # some initialization
        ...
        {:ok, initialized_st}
      end
    end
    
    ...
    
    st = %SessionStateImpl{}
    {:ok, pid} = Session.start_link(st, args, ...)
    


    이제 우리는 초기화되지 않은 상태를 전달해야 합니다. 제가 별로 좋아하지 않는 아이디어입니다.

    동작 사용



    이것으로 놀면서 나는 이상한 일을하고 있고 가능한 사용자를 혼란스럽게 할 것이라고 결론 지었습니다.

    나는 전통적인 행동으로 옮겼고 모든 것이 옳았다는 느낌을 받았습니다. :)

    프로토콜 사용의 성공적인 예



    일단 git 커밋을 분석하는 시스템을 개발 중이었습니다. 커밋은 Bitbucket API에서 가져왔고 전달되는 방대한 JSON 데이터 청크를 나타냅니다.
    Commit 프로토콜을 추가하고 APICommit 구현했습니다.

    defprotocol Commit do
      def author(commit)
      def author_email(commit)
      def added_files(commit)
      ...
    end
    


    나중에 그런 커밋을 DB에 다르게 저장된 커밋과 결합해야 했습니다. 그러나 프로토콜이 있는 덕분에 이 논리는 Commit 데이터 구조에 대해 DBCommit를 구현한 후 다르게 취급할 필요가 없었습니다.

    이 프로토콜 사용 사례는 순수 데이터 구조를 다루는 예이며 성공적인 것으로 판명되었습니다.

    결론



    Elixir에서 동작과 프로토콜 중에서 선택하는 문제는 흥미롭고 동시에 도전적입니다.

    행동 및 프로토콜을 사용한 모델링 시도의 성공 및 실패 사례를 더 많이 볼 수 있기를 바랍니다.

    좋은 웹페이지 즐겨찾기