Behaviours permitem definir interfaces (conjuntos de funções e macros) em um módulo de behaviour que depois podem ser implementados por diferentes módulos de callback. Graças à interface compartilhada, esses módulos de callback podem ser usados de forma intercambiável.
Repare na grafia britânica de "behaviours".
Para definir um behaviour, precisamos criar um novo módulo e especificar uma lista de funções que fazem parte da interface desejada. Cada função precisa ser definida com o atributo de módulo @callback. A sintaxe é idêntica à de uma typespec de função (@spec). Precisamos especificar um nome de função, uma lista de tipos de argumento e todos os tipos de retorno possíveis.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Para adicionar um behaviour existente ao nosso módulo (criar um módulo de callback), usamos o atributo de módulo @behaviour. Seu valor deve ser o nome do módulo de behaviour que estamos adicionando.
Depois, precisamos definir todas as funções (callbacks) exigidas por esse módulo de behaviour. Se estivermos implementando o behaviour de outra pessoa, como os behaviours Access ou GenServer que já vêm no Elixir, encontraríamos a lista de todos os callbacks desse behaviour na documentação em hexdocs.pm.
Um módulo de callback não se limita a implementar apenas as funções que fazem parte do seu behaviour. Também é possível que um único módulo implemente vários behaviours.
Para marcar qual função vem de qual behaviour, devemos usar o atributo de módulo @impl antes de cada função. Seu valor deve ser o nome do módulo de behaviour que define esse callback.
defmodule BookCollection do
@behaviour Countable
defstruct [:list, :owner]
@impl Countable
def count(collection) do
Enum.count(collection.list)
end
def mark_as_read(collection, book) do
# other function unrelated to the Countable behaviour
end
end
Ao definir um behaviour, é possível fornecer uma implementação padrão de um callback. Essa implementação deve ser definida na quoted expression da macro __using__/1. Para que seja possível para quem usa o módulo de behaviour substituir a implementação padrão, chame a macro defoverridable/1 depois da implementação da função. Ela aceita uma lista de palavras-chave com nomes de funções como chaves e aridades de função como valores.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
defmacro __using__(_) do
quote do
@behaviour Countable
def count(collection), do: Enum.count(collection)
defoverridable count: 1
end
end
end
Repare que definir funções dentro de __using__/1 é desaconselhado para qualquer finalidade que não seja definir implementações padrão de callbacks, mas você sempre pode definir funções em outro módulo e importá-las na macro __using__/1.