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:

Estrutura do 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.

Dependências como Bibliotecas

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.

This post is also available on DEV.