DEV Community

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

Posted on

2 1

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.

Image of AssemblyAI

Automatic Speech Recognition with AssemblyAI

Experience near-human accuracy, low-latency performance, and advanced Speech AI capabilities with AssemblyAI's Speech-to-Text API. Sign up today and get $50 in API credit. No credit card required.

Try the API

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay