Mit Behaviours können wir Schnittstellen (Mengen von Funktionen und Makros) in einem Behaviour-Modul definieren, die später von verschiedenen Callback-Modulen implementiert werden können. Dank der gemeinsamen Schnittstelle lassen sich diese Callback-Module austauschbar verwenden.
Beachte die britische Schreibweise von „behaviours“.
Um ein Behaviour zu definieren, müssen wir ein neues Modul erstellen und eine Liste von Funktionen angeben, die Teil der gewünschten Schnittstelle sind. Jede Funktion muss mit dem Modulattribut @callback definiert werden. Die Syntax ist identisch mit der einer Funktionstypspezifikation (@spec). Wir müssen einen Funktionsnamen, eine Liste von Argumenttypen und alle möglichen Rückgabetypen angeben.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Um unserem Modul ein vorhandenes Behaviour hinzuzufügen (ein Callback-Modul zu erstellen), verwenden wir das Modulattribut @behaviour. Sein Wert sollte der Name des Behaviour-Moduls sein, das wir hinzufügen.
Dann müssen wir alle Funktionen (Callbacks) definieren, die dieses Behaviour-Modul verlangt. Wenn wir ein Behaviour von jemand anderem implementieren, etwa die in Elixir eingebauten Behaviours Access oder GenServer, finden wir die Liste aller Callbacks des Behaviours in der Dokumentation auf hexdocs.pm.
Ein Callback-Modul ist nicht darauf beschränkt, nur die Funktionen zu implementieren, die Teil seines Behaviours sind. Es ist auch möglich, dass ein einzelnes Modul mehrere Behaviours implementiert.
Um zu kennzeichnen, welche Funktion aus welchem Behaviour stammt, sollten wir vor jeder Funktion das Modulattribut @impl verwenden. Sein Wert ist der Name des Behaviour-Moduls, das diesen Callback definiert.
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
Beim Definieren eines Behaviours ist es möglich, eine Standardimplementierung für einen Callback anzugeben. Diese Implementierung sollte im quoted expression des Makros __using__/1 definiert werden. Damit Nutzer des Behaviour-Moduls die Standardimplementierung überschreiben können, sollten wir nach der Funktionsimplementierung das Makro defoverridable/1 aufrufen. Es erwartet eine Keyword-Liste, in der Funktionsnamen die Schlüssel und die Stelligkeiten der Funktionen die Werte sind.
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
Beachte, dass es nicht empfehlenswert ist, Funktionen in __using__/1 zu definieren, außer um Standardimplementierungen von Callbacks bereitzustellen. Du kannst Funktionen aber jederzeit in einem anderen Modul definieren und sie im Makro __using__/1 importieren.