DEV Community

Cover image for Dependências: Bibliotecas ou OTP Applications?
Mário Melo
Mário Melo

Posted on

Dependências: Bibliotecas ou OTP Applications?

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.

Top comments (0)