Percorrendo listas agrupadas com Rails
Facilitando a iteração por registros agrupados
Saber percorrer uma lista de dados, de forma eficiente, é de fundamental importância na construção de um algoritmo. Não é raro encontrar exemplos de códigos que priorizam a performance na manipulação dessas listas. Contudo, a legibilidade muitas vezes é sacrificada em prol da máxima eficiência.
Sob essa óptica, o Rails oferece alguns métodos de manipulação de Arrays que economizam tempo de desenvolvimento de uma eventual implementação própria.
O in_groups_of
, por exemplo, mostra-se bastante útil quando desejamos percorrer registros agrupados por um determinado limite. Combinado com alguns métodos do Active Record como o group_by
e scope
, a consulta pode se tornar ainda mais poderosa.
Para ilustrar o uso, podemos pensar em um contexto de Lobby em algum jogo eletrônico.
Nossa regra de negócio é descrita como:
- Cada jogador possui um nível númerico.
- Cada partida permite um número máximo de 10 jogadores (sem times; FFA).
- As partidas são niveladas pelo nível dos jogadores, de forma que jogadores de níveis idênticos fiquem numa mesma partida.
Comecemos pela classe de jogador:
class Player < ApplicationRecord
validates :level, presence: true
validates :level, numericality: { only_integer: true }
belongs_to :lobby, optional: true
scope :ordered_by_level, lambda {
order(level: :desc)
}
def self.group_by_level
ordered_by_level.group_by(&:level)
end
end
Na classe, validamos a presença e a numericidade inteira, além de adicionar uma associação opcional a um Lobby. O scope ordered_by_level ordena os registros de pelo nível do jogador, de forma descendente, enquanto o método de classe group_by_level agrupa os resultados em uma Hash, onde cada key é o nível do jogador, e os valores, os própria instância dos jogadores:
# Player.group_by_level
{
50 =>
[#<Player:0x0000000108733120
id: 20,
name: 'Paul Atreides',
level: 50,
lobby_id: nil>,
#<Player:0x0000000108733142
id: 104,
name: 'Stilgar',
level: 50,
lobby_id: nil>
],
48 =>
[#<Player:0x0000000108733119
id: 2,
name: 'Chani',
level: 48,
lobby_id: nil>,
]
# ...
}
Também precisamos de um modelo de Lobby:
class Lobby < ApplicationRecord
has_many :players, dependent: :nullify
end
Agora, vamos ao coração da lógica. Necessitamos de um algoritmo que capture a lista já ordenada por níveis de jogadores, separe essa lista em uma outra lista agrupada pelo limite máximo em cada partida, e associe um novo lobby a esses jogadores. A leitura da documentação do método in_groups_of é recomendada para entender plenamente o código a seguir.
class LobbyService
PLAYER_LIMIT_BY_ROOM = 10
def self.call
new.call
end
def call
distribute_players_in_rooms
end
private
def distribute_players_in_rooms
grouped_players.each do |_level, players_list|
InsertPlayersInLobbyJob.perform_later(players_list: players_list.in_groups_of(PLAYER_LIMIT_BY_ROOM, false),
lobby:)
end
end
def grouped_players
Player.group_by_level
end
def lobby
Lobby.create!
end
end
Por fim, basta um Job que associe o lobby aos jogadores:
class InsertPlayersInLobbyJob < ApplicationJob
queue_as :default
def perform(players_list:, lobby:)
players_list.each { |player| player.lobby = lobby }
end
end
Com isso, teremos lobbies nivelados pelo nível dos jogadores, utilizando-se principalmente de métodos que percorrem listas, preservando a legibilidade e manutenibilidade do código!
Até a próxima!
Eloá
Excelente!!
Thiago Portella
Obrigado!!! ❤️
Eduardo
Desafio agora a fazer um algoritmo pra sortear os times da pelada de fim de ano, de forma a manter os times equilibrados de acordo com o nível dos jogadores. 😀
Thiago Portella
O maior desafio vai ser descobrir qual o nível de cada um 😆