DEV Community

Cover image for Behaviours em Elixir
Willian Frantz
Willian Frantz

Posted on • Edited on

Behaviours em Elixir

Assim como Protocols, Behaviours também são uma forma de manter um padrão de interface/herança e até mesmo polimorfismo no Elixir.

Vale lembrar que Elixir é uma linguagem de programação funcional, portanto, não é o nosso forte o conceito de objeto, classe e métodos. No final, sempre estaremos falando sobre módulos e funções!

Então de que me serve possuir uma abstração para criar interfaces em uma tecnologia como esta?

Cenário

Imaginem que vamos desenvolver uma aplicação onde a pessoa insere seus dados para consultar todos seus cartões de crédito, verificar se as faturas estão em dia, e inclusive gerar possíveis estratégias financeiras, etc...

Para isso precisamos criar uma API REST que irá consultar de diversas fontes de cartões para validar os dados e processar as respostas finais.

Você consegue identificar um padrão nisso? Se precisamos consultar mais de uma API externa para reunir os dados para a resposta final, isso significa que nosso código terá muitos módulos que serão correspondentes a provedores diferentes (ex: Itaú, Nubank, etc...)

Problema

Como iremos mapear todos esses módulos de provedores que vamos criar dentro do nosso sistema então? Como garantir que eles precisam ter uma execução semelhante, retornando os dados processados da mesma forma?

Tendo em vista que as APIs externas podem ser distintas, possuir dados diferentes e respostas diferentes, isso pode se tornar um problema e pode nos levar a criar códigos aleatórios para solucionar cada tipo de API de uma forma desnecessária.

Behaviours

Com behaviours conseguimos criar um padrão comportamental, definindo uma assinatura pré-exigida pelo módulo, garantindo que todos os módulos subsequentes que irão se especializar naquela API precisarão implementar aqueles requisitos.

Em outras palavras, considere sendo essa a nossa interface de acesso à APIs externas:

defmodule Bank.API do
  @moduledoc """
  Este módulo é responsável por definir uma interface 
  padrão para os demais módulos de acesso ao 
  Banco implementarem.
  """

  @type params() :: map()
  @type response() :: {:ok, map()} | {:error, String.t()}

  @doc """
  Este método irá acessar a base externa e
  retornar os dados bancários da pessoa usuária
  """
  @callback call(params()) :: response()
end
Enter fullscreen mode Exit fullscreen mode

Ignorando os moduledocs e typespecs do nosso módulo, nos resta essa @callback, que é exatamente o comando utilizado para definir as funções que deverão ser implementadas pelo módulo que pretende se especializar nessa API.

Perceba o quão genérico nossa interface ficou, ela só da a entender que é uma API de Banco, com uma função de chamada. Em momento algum foi especificado qual o tipo de banco, ou como essa chamada será feita (via: API, gRPC, SOAP, etc...), isso será total responsabilidade de quem for implementar essa interface.

Agora com nosso behaviour bem definido, podemos seguir para a implementação do primeiro Client que iremos utilizar em nossa aplicação para consultar os dados:

defmodule Bank.Nubank do
  @moduledoc """
  Módulo que implementa a requisição para a API do Nubank.
  """

  @behaviour Bank.API

  @impl true
  def call(%{user_id: id}) do
    id
    |> nubank_url()
    |> HTTPoison.get()
    |> case do
      {:ok, %{status_code: 200, body: response}} ->
        {:ok, response}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp nubank_url(id), do:
      Application.get_env(:my_app, :nubank)[:url] <> "/user/#{id}"
end
Enter fullscreen mode Exit fullscreen mode

(a requisição acima é meramente ilustrativa 😅)

O comando @behaviour Bank.API descreve que nosso módulo Nubank irá se especializar no Bank.API, logo, ele deverá implementar a seguinte função: call/1.

Com isto, temos nosso primeiro provedor implementado, yaay 🚀!

Conclusão

Gosto de dizer que estamos definindo padrões comportamentais, com isso, quando vemos que um módulo específico implementa um Behaviour, já dá para ter uma ideia de quais funções ele roda, qual o seu propósito, como ele deve ser implementado em termos de input e output.

Esse tipo de padronização ajuda a escalar nosso código, diminuir a curva de aprendizagem para novas pessoas desenvolvedoras e inclusive é uma ótima ferramenta para auto-documentar nosso código, deixa tudo mais descritivo e explícito!

E você, o que acha de behaviours? Gostaria de complementar, adicionar ou remover alguma informações deste tópico?

that's all

Top comments (5)

Collapse
 
elixir_utfpr profile image
Elixir UTFPR (por Adolfo Neto)

Oi Willan,

Parabéns pelo texto!

Fiquei em dúvida quanto a "todos os módulos subsequentes que irão herdar daquela especialidade".

"Herdar" seria o termo correto?

Parece-me que quando um módulo diz "@behaviour Bank.API", ele está meio que assiando um contrato se comprometendo a implementar as funções cuja assinatura está em "Bank.API". É isso?

Se for isto, não seria melhor dizer:
"todos os módulos subsequentes que irão assinar o contrato daquele comportamento"?

Nunca usei Behaviours. É uma dúvida mesmo.

Collapse
 
wlsf profile image
Willian Frantz

Herdar é um péssimo nome mesmo, haha! Acho que usei ele porque comecei pensando sobre a semelhança de behaviours com interfaces.

Obrigado pelo comentário professor!

Collapse
 
dii_lua profile image
Letícia Silva

Arrasou, amei a explicação!!! 👏 👏 ❤️

Collapse
 
elixir_utfpr profile image
Elixir UTFPR (por Adolfo Neto)

Seria interessante ter outro exemplo de módulo que implementa o behaviour.

Collapse
 
maiquitome profile image
Dev Maiqui 🇧🇷

O artigo ficou ótimo Willian, obrigado pela explicação :)