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.
La 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.