En Elixir, los behaviors o comportamientos son una forma de definir especificaciones o contratos que un módulo debe cumplir para ser considerado de un tipo concreto. Esto es muy parecido a las interfaces en otros lenguajes como Java, Go o TypeScript. La idea principal es que un módulo que implemente un behavior debe proporcionar ciertas funciones con nombres y firmas específicas, garantizando así que puede ser utilizado de manera uniforme, independientemente de su implementación interna.
Un ejemplo clásico en Elixir es el GenServer, que es un behavior que exige que el módulo que lo implemente tenga funciones como init, handle_call o handle_cast. No podemos cambiar el nombre de estas funciones porque el sistema las invoca directamente. Esto asegura que cualquier módulo que sea un GenServer se comporte de manera predecible y compatible con el ecosistema OTP.
Para entender mejor cómo funcionan los behaviors, podemos crear uno propio. Imaginemos que queremos definir un comportamiento para módulos que se encarguen de saludar. Este behavior se llamará Saludador y especificará que cualquier módulo que lo implemente debe tener una función saludar/1 que reciba un nombre y devuelva un saludo en forma de cadena.
Declarar este behavior implica definir un callback con la anotación @callback, que es una forma de especificar la firma de la función que debe implementarse. Por ejemplo:
defmodule Comportamiento.Saludador do
@callback saludar(String.t()) :: String.t()
end
Con esta definición, cualquier módulo que quiera ser un Saludador debe implementar la función saludar/1.
A partir de aquí, podemos crear distintas implementaciones que cumplan este contrato. Por ejemplo, un saludo informal y otro formal:
defmodule Comportamiento.SaludoInformal do
@behaviour Comportamiento.Saludador
@impl true
def saludar(nombre) do
"hey, #{nombre}"
end
end
defmodule Comportamiento.SaludoFormal do
@behaviour Comportamiento.Saludador
@impl true
def saludar(nombre) do
"Buenos días, encantado, #{nombre}"
end
end
Aquí usamos la directiva @behaviour para indicar que estos módulos implementan el comportamiento Saludador. Esto permite que el compilador nos avise si olvidamos implementar alguna función requerida. Además, la anotación @impl true señala que la función es una implementación de un callback definido en el behavior.
Para facilitar el uso de estos saludadores, podemos crear una función que reciba el módulo que implementa el comportamiento y el nombre, y simplemente delegue la llamada a la función saludar/1 de ese módulo:
defmodule Comportamiento.Saludador do
@callback saludar(String.t()) :: String.t()
def saludar(modulo, nombre) do
modulo.saludar(nombre)
end
end
Así, podemos llamar a Comportamiento.Saludador.saludar(Comportamiento.SaludoFormal, "Dani") y obtener "Buenos días, encantado, Dani", o usar Comportamiento.Saludador.saludar(Comportamiento.SaludoInformal, "Dani") para un saludo más casual.
Este enfoque nos permite diseñar sistemas flexibles y extensibles, donde diferentes módulos pueden cumplir el mismo contrato pero ofrecer comportamientos distintos. Además, el compilador y herramientas como Dialyzer nos ayudan a asegurar que las implementaciones cumplen con el contrato definido.
Volviendo a los behaviors integrados en Elixir, como GenServer, podemos ver que ellos también definen callbacks obligatorios y opcionales. Por ejemplo, handle_info es un callback que no siempre es necesario implementar, y el propio GenServer proporciona implementaciones por defecto para evitar errores si no se define.
Un detalle curioso es que en Elixir se usa la ortografía británica @behaviour con ou en lugar de la americana @behavior. Esto es simplemente una convención del lenguaje y hay que tenerlo en cuenta al declarar behaviors.
En definitiva, los behaviors en Elixir son una herramienta poderosa para definir contratos claros entre módulos, facilitando la creación de sistemas modulares y mantenibles, donde podemos intercambiar implementaciones sin preocuparnos por los detalles internos, siempre que cumplan con la interfaz establecida.