O projeto: Scrumchkin Online
Há cerca de um ano atrás criei um jogo de cartas para ensinar Scrum: o Scrumchkin. O jogo tornou o processo de aprendizado mais divertido e foi adotado por Scrum Trainers de diversos países, até que a pandemia inviabilizou qualquer turma presencial.
E foi daí que surgiu meu projeto pessoal: criar uma versão online do Scrumchkin. O que seria uma ótima oportunidade para brincar e aprender mais sobre Phoenix Liveview.
Inicialmente, pensei na seguinte estrutura para o projeto:
Desta forma, seria possível criar jogos em processos separados e ter um registro com identificadores únicos de cada jogo para que cada partida pudesse ser acessada através de uma URL diferente.
Exemplo:
O usuário acessa a URL
http://scrumchkin.com/game/abc123
A aplicação web pergunta ao Registro de Jogos onde está o jogo
abc123
O Registro de Jogos encontra o PID da partida e retorna para aplicação web
O Registro de Jogos como uma biblioteca
Tendo em mente o princípio da responsabilidade única, o desenho acima deixa bem evidente a existência de 3 projetos diferentes: O Registro de Jogos, o Servidor de Jogos e a Interface Web.
Os próximos parágrafos vão falar sobre alguns aspectos técnicos de Elixir a título de curiosidade. Se você quiser apenas entender a diferença entre uma biblioteca e uma aplicação OTP basta pular esta parte :)
Tecnicamente o Registro de Jogos é extremamente simples: ele vincula um ID único a uma partida. Ele é basicamente um dicionário que tem como chave um UUID e como valor um PID de um GenServer para uma partida.
Inicialmente, criei o Registro de Jogos como uma biblioteca capaz de fazer operações CRUD em uma tabela 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 - A biblioteca armazena o estado atual de partidas e as vincula a um código identificador. Ela é capaz de listar, obter, salvar e deletar partidas do registro.
Um pequeno problema
Para que eu pudesse utilizar a tabela ets, ela precisava existir. Isto significa que em algum momento a função
init
do código acima precisaria ser chamada pela minha aplicação web.
def init() do
:ets.new(:scrumchkin, [:set, :public, :named_table])
end
Mas isso vai contra o princípio de responsabilidade única que utilizei para dividir este projeto em partes menores, certo?
O Registro como uma aplicação
Mas o que é uma dependência como biblioteca? Ela é uma engrenagem que faz parte de um todo; algo bem parecido com uma peça de Lego. Sabemos onde estão os pinos e buracos e a utilizamos para construir algo maior.
A dependência de uma aplicação OTP é um pouco diferente.
Pense em um carro. Geralmente, carros têm um mecanismo de refrigeração do motor que é iniciado no momento em que você vira a chave e dá a partida. O carro depende deste mecanismo para funcionar, mas ele é um tanto quanto independente: muitas vezes ele é acionado quando desligamos o carro (aquele barulho de ventilador que vem de debaixo do capô, principalmente em dias quentes).
Esse mecanismo de refrigeração tem interfaces com o motor do carro, mas controla seu próprio estado. Existe uma relação clara de dependência, mas não de controle. O motor depende do sistema de refrigeração para não superaquecer, mas não o controla.
E o mesmo precisava acontecer com meu Registro de Jogos, que ficou assim:
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("Tabela scrumchkin criada")
{: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
Mas... o que muda?
Minha aplicação web não é responsável por criar a tabela ets. Ela apenas diz que depende do Registro de Jogos e que ele é agora uma aplicação extra.
A alteração no arquivo
mix.exs
é simples:
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
Agora, toda vez que inicio minha aplicação com um
mix phx.server
omeu registro de jogos é iniciado automaticamente e assume a responsabilidade de criar a tabela ets onde vai armazenar os PIDs das partidas de Scrumchkin.
A minha aplicação web depende do Registro de Jogos, mas confia que ele consegue resolver seus problemas sozinho.