Do you want to pick up from where you left of?
Take me there

Comportamientos

Aprendimos acerca de Typespecs en la lección anterior, ahora vamos a aprender como requerir que un módulo implemente esas especificaciones. En Elixir, esta funcionalidad es referida como comportamientos.

Usos

A veces quieres que los módulos compartan una API pública, la solución para esto en Elixir son los comportamientos. Los comportamientos tienen dos roles importantes:

Elixir incluye un número de comportamientos tales como GenServer pero en esta lección vamos a enfocarnos en crear uno propio.

Definiendo un comportamiento

Para mejor entendimiento de los comportamientos vamos a implementar uno para un módulo worker. Se espera que estos workers implementen dos funciones: init/1 y perform/2.

Para lograr esto vamos a usar la directiva @callback la cual tiene una sintaxis similar a @spec. Esto define una función requerida, para macros podemos usar @macrocallback. Vamos a especificar las funciones init/1 y perform/2 para nuestros workers.

defmodule Example.Worker do
  @callback init(state :: term) :: {:ok, new_state :: term} | {:error, reason :: term}
  @callback perform(args :: term, state :: term) ::
              {:ok, result :: term, new_state :: term}
              | {:error, reason :: term, new_state :: term}
end

Aquí hemos definido init/1 que acepta cualquier valor y retorna una tupla que puede ser {:ok, state} o {:error, reason}, esta inicialización es bastante estándar. Nuestra función perform/2 recibirá algunos argumentos para el worker junto con el estado que inicializamos, esperaremos que perform/2 retorne {:ok, result, state} o {:error, reason, state} bastante similar a los GenServers.

Usando los comportamientos

Ahora que hemos definido nuestro comportamiento podemos usarlo para crear una variedad de módulos que comparten la misma API pública. Agregar un comportamiento a nuestro módulo es fácil con el atributo @behaviour.

Usando nuestro nuevo comportamiento vamos a crear un módulo cuya tarea sea descargar un archivo remoto y guardarlo localmente:

defmodule Example.Downloader do
  @behaviour Example.Worker

  def init(opts), do: {:ok, opts}

  def perform(url, opts) do
    url
    |> HTTPoison.get!()
    |> Map.fetch(:body)
    |> write_file(opts[:path])
    |> respond(opts)
  end

  defp write_file(:error, _), do: {:error, :missing_body}

  defp write_file({:ok, contents}, path) do
    path
    |> Path.expand()
    |> File.write(contents)
  end

  defp respond(:ok, opts), do: {:ok, opts[:path], opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

¿O qué tal un worker que comprime un arreglo de archivos? Eso es posible también:

defmodule Example.Compressor do
  @behaviour Example.Worker

  def init(opts), do: {:ok, opts}

  def perform(payload, opts) do
    payload
    |> compress
    |> respond(opts)
  end

  defp compress({name, files}), do: :zip.create(name, files)

  defp respond({:ok, path}, opts), do: {:ok, path, opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

Mientras el trabajo es realizado diferente la API pública no lo es, y cualquier código que aproveche estos módulos puede interactuar con ellos sabiendo como van a responder. Esto nos da la habilidad de crear cualquier número de workers, todos realizando tareas diferentes pero conforme a la misma API pública.

Si por casualidad agregamos un comportamiento pero no implementamos todas las funciones requeridas tendremos una advertencia en tiempo de compilación. Para ver esto en acción vamos a modificar el código de Example.Compressory vamos a eliminar la función init/1:

defmodule Example.Compressor do
  @behaviour Example.Worker

  def perform(payload, opts) do
    payload
    |> compress
    |> respond(opts)
  end

  defp compress({name, files}), do: :zip.create(name, files)

  defp respond({:ok, path}, opts), do: {:ok, path, opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

Ahora cuando compilemos nuestro código deberíamos ver una advertencia:

lib/example/compressor.ex:1: warning: undefined behaviour function init/1 (for behaviour Example.Worker)
Compiled lib/example/compressor.ex

Eso es todo. Ahora estamos listos para construir y compartir comportamientos con otros.

¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!