Dipendenze: Librerie o applicazioni OTP

Il progetto: Scrumchkin Online

Circa un anno fa ho creato un gioco di carte per insegnare Scrum: Scrumchkin. Il gioco ha reso il processo di apprendimento più divertente ed è stato adottato da Scrum Trainer di diversi paesi, finché la pandemia non ha reso impossibile qualsiasi lezione in presenza.

Ed è da lì che è nato il mio progetto personale: creare una versione online di Scrumchkin. Che sarebbe stata un’ottima opportunità per giocare e imparare di più su Phoenix Liveview.

Inizialmente, ho pensato alla seguente struttura per il progetto:

In questo modo, sarebbe possibile creare giochi in processi separati e avere un registro con identificatori unici per ogni gioco in modo che ogni partita potesse essere accessibile attraverso un URL diverso.

Esempio:

  • L’utente accede all’URL http://scrumchkin.com/game/abc123

  • L’applicazione web chiede al Registro dei Giochi dove si trova il gioco abc123

  • Il Registro dei Giochi trova il PID della partita e lo restituisce all’applicazione web

Il Registro dei Giochi come libreria

Tenendo a mente il principio di responsabilità singola, il design sopra rende evidente l’esistenza di 3 progetti diversi: Il Registro dei Giochi, il Server di Gioco e l’Interfaccia Web.

I prossimi paragrafi parleranno di alcuni aspetti tecnici di Elixir come curiosità. Se vuoi solo capire la differenza tra una libreria e un’applicazione OTP sentiti libero di saltare questa parte :)

Tecnicamente il Registro dei Giochi è estremamente semplice: collega un ID unico a una partita. È fondamentalmente un dizionario che ha un UUID come chiave e un PID di un GenServer come valore per una partita.

Inizialmente, ho creato il Registro dei Giochi come una libreria capace di eseguire operazioni CRUD su una tabella ets:

defmodule GameRegister do
  def init() do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
  end

  def save(value) do
    key = UUID.uuid1()
    :ets.insert_new(:scrumchkin, {key, value})
    key
  end

  def delete(key) do
    :ets.delete(:scrumchkin, key)
  end

  def get(key) do
    :scrumchkin
    |> :ets.lookup(key)
    |> format_result
  end

  def list_all do
    :ets.tab2list(:scrumchkin)
  end

  defp format_result([]), do: {:error, "Game not found"}

  defp format_result(item_list) do
    item_list
    |> hd
  end
end

TL;DR - La libreria memorizza lo stato corrente delle partite e le collega a un codice identificatore. È capace di elencare, ottenere, salvare ed eliminare partite dal registro.

Un piccolo problema

Per poter usare la tabella ets, doveva esistere. Questo significa che a un certo punto la funzione init dal codice sopra avrebbe dovuto essere chiamata dalla mia applicazione web.

  def init() do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
  end

Ma questo va contro il principio di responsabilità singola che ho usato per dividere questo progetto in parti più piccole, giusto?

Il Registro come applicazione

Ma cos’è una dipendenza come libreria? È un ingranaggio che fa parte di un tutto; qualcosa di molto simile a un pezzo di Lego. Sappiamo dove sono i perni e i buchi e lo usiamo per costruire qualcosa di più grande.

Dipendenze come LibrerieLa dipendenza da un’applicazione OTP è un po’ diversa.

Pensa a un’auto. Di solito, le auto hanno un meccanismo di raffreddamento del motore che si avvia quando giri la chiave e avvii l’auto. L’auto dipende da questo meccanismo per funzionare, ma è in qualche modo indipendente: molte volte si attiva quando spegniamo l’auto (quel rumore di ventola che viene da sotto il cofano, specialmente nelle giornate calde).

Questo meccanismo di raffreddamento ha interfacce con il motore dell’auto, ma controlla il proprio stato. C’è una chiara relazione di dipendenza, ma non di controllo. Il motore dipende dal sistema di raffreddamento per non surriscaldarsi, ma non lo controlla.

E lo stesso doveva succedere con il mio Registro dei Giochi, che è diventato così:

defmodule GameRegister do
  use GenServer

  def start_link(state) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(stack) do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
    IO.puts("Tabella scrumchkin creata")
    {:ok, stack}
  end

  def handle_call({:save, game}, _from, state) do
    key = UUID.uuid1()
    :ets.insert_new(:scrumchkin, {key, game})
    {:reply, key, state}
  end

  def handle_call({:delete, game_id}, _from, state) do
    :ets.delete(:scrumchkin, game_id)
    {:reply, :ok, state}
  end

  def handle_call({:get, game_id}, _from, state) do
    result =
      :scrumchkin
      |> :ets.lookup(game_id)
      |> format_result

    {:reply, result, state}
  end

  def handle_call(:list_all, _from, state) do
    {:reply, :ets.tab2list(:scrumchkin), state}
  end

  def save(game) do
    GenServer.call(__MODULE__, {:save, game})
  end

  def delete(game_id) do
    GenServer.call(__MODULE__, {:delete, game_id})
  end

  def get(game_id) do
    GenServer.call(__MODULE__, {:get, game_id})
  end

  def list_all do
    GenServer.call(__MODULE__, :list_all)
  end

  defp format_result([]), do: {:error, "Game not found"}

  defp format_result(item_list) do
    item_list
    |> hd
  end
end

Ma… cosa cambia?

La mia applicazione web non è responsabile della creazione della tabella ets. Dice solo che dipende dal Registro dei Giochi e che ora è un’applicazione extra.

La modifica nel file mix.exs è semplice:

  def application do
    [
      mod: {Scrumchkin.Application, []},
      extra_applications: [:logger, :runtime_tools, :game_register, :game_engine]
    ]
  end
  defp deps do
    [
      {:game_engine, path: "../game_engine"},
      {:game_register, path: "../game_register"}
    ]
  end

Ora, ogni volta che avvio la mia applicazione con mix phx.server, il mio registro dei giochi viene automaticamente avviato e si assume la responsabilità di creare la tabella ets dove memorizzerà i PID delle partite di Scrumchkin.

La mia applicazione web dipende dal Registro dei Giochi, ma si fida che possa risolvere i suoi problemi da solo.

La pericolosa cultura della vergogna del fallimento
Una riflessione sui miei 20 anni di lavoro nello sviluppo software