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
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
(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?
Top comments (5)
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.
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!
Arrasou, amei a explicação!!! 👏 👏 ❤️
Seria interessante ter outro exemplo de módulo que implementa o behaviour.
O artigo ficou ótimo Willian, obrigado pela explicação :)