Comportamientos

Los comportamientos es la respuesta de Elixir a la programación por contrato y a la fabricación de interfaces que encontramos en otros lenguajes de programación. Mediante callback y behaviour podemos crear módulos que especifican primitivas que otros módulos deben implementar.

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.

Lista de reproducción
  1. 1
    mix
    10 minutos
  2. 2
    Documentando código: comentarios, docs y moduledocs
    10 minutos
  3. 3
    Atributos de módulo
    9 minutos
  4. 4
    Dependencias
    12 minutos
  5. 5
    Un ejemplo práctico de módulo útil
    13 minutos
  6. 6
    Alias e import
    10 minutos
  7. 7
    Sobre las macros, require y use
    11 minutos
  8. 8
    Typespecs (parte 1, usando tipos básicos)
    10 minutos
  9. 9
    Typespecs (parte 2, tipos propios y t())
    11 minutos
  10. 10
    Comportamientos
    11 minutos
  11. 11
    Tratamiento de errores con rescue
    8 minutos
  12. 12
    Elevando errores con raise
    8 minutos
  13. 13
    with
    14 minutos
  14. 14
    Sigilos
    8 minutos
  15. 15
    Tests con ExUnit
    12 minutos
  16. 16
    Más particularidades de ExUnit
    13 minutos
  17. 17
    Microservicios en Elixir con Plug
    11 minutos
  18. 18
    Cómo Plug.Router te ayuda a escribir microservicios en Elixir
    14 minutos
  19. 19
    ¿Cómo hacer rutas dinámicas en Phoenix y Plug?
    13 minutos