GenServer 모듈 분리하기
GenServer 모듈 분리하기
Conding Gnome 강의에서 나왔던 이야기 한소절. 흥미로워서 기록해둠
다음은 대부분의 elixir 사용자들이 짜는 코드다. Game 서버를 만들고 Game의 로직을 집어넣는다.
defmodule Hangman.Game do
defstruct(
turns_left: 7,
game_state: :initializing,
letters: [],
used: MapSet.new()
)
use GenServer
alias Hangman.Game
##############################
# Hangman API #
#########################
def new_game() do
{:ok, pid} = Supervisor.start_child(Hangman.Supervisor, [])
pid
end
def make_move(game_pid, guess) do
GenServer.call(game_pid, {:make_move, guess})
end
def tally(game_pid) do
GenServer.call(game_pid, {:tally})
end
##################################
# GenServer initialization #
################################
def start_link() do
GenServer.start_link(__MODULE__, Dictionary.random_word)
end
def start_link(word) do
GenServer.start_link(__MODULE__, word)
end
##################################
# GenServer initialization #
################################
def init(word) do
{:ok, create_state(word)}
end
def handle_call({:make_move, guess}, _from, game) do
do_make_move(game, guess)
|> reply_call()
end
def handle_call({:tally}, _from, game) do
{game, do_tally(game)}
|> reply_call()
end
defp reply_call({game, tally}) do
{:reply, tally, game}
end
##################################
# Game implementation #
################################
defp do_make_move(game = %{game_state: state}, _guess) when state in [:won, :lost] do
return_with_tally(game)
end
def do_make_move(game, guess) do
accept_move(game, guess, MapSet.member?(game.used, guess))
|> return_with_tally()
end
def do_tally(game) do
%{
game_state: game.game_state,
turns_left: game.turns_left,
used: game.used |> MapSet.to_list() |> Enum.sort()
letters: game.letters |> reveal_guessed(game.used)
}
end
defp create_state(word) do
%Hangman.Game{
letters: word |> String.codepoints()
}
end
defp accept_move(game, _guess, _already_guessed = true) do
Map.put(game, :game_state, :already_used)
end
defp accept_move(game, guess, _already_guessed) do
Map.put(game, :used, MapSet.put(game.used, guess))
|> score_guess(Enum.member?(game.letters, guess))
end
defp score_guess(game, _good_guess = true) do
new_state =
MapSet.new(game.letters)
|> MapSet.subset?(game.used)
|> maybe_won()
Map.put(game, :game_state, new_state)
end
defp score_guess(game = %{turns_left: 1}, _not_good_guess = false) do
Map.put(game, :game_state, :lost)
end
defp score_guess(game = %{turns_left: turns_left}, _not_good_guess = false) do
%{
game
| game_state: :bad_guess,
turns_left: turns_left - 1
}
end
defp reveal_guessed(letters, used) do
letters
|> Enum.map(fn letter -> reveal_letter(letter, MapSet.member?(used, letter)) end)
end
defp reveal_letter(letter, _in_word = true), do: letter
defp reveal_letter(_letter, _not_in_word), do: "_"
defp maybe_won(true), do: :won
defp maybe_won(_), do: :good_guess
defp return_with_tally(game), do: {game, tally(game)}
end
서버 api 로직과 Game api 로직이 모두 모여서 알아보기 어렵게 만든다. 함수 각각이 api용 함수인지 비즈니스 로직용 함수인지 구분도 어렵다. 강사는 다음과 같이 서버 모듈과 비즈니스 로직 모듈을 나누길 추천한다.
# hangman/server.ex
defmodule Hangman.Server do
use GenServer
alias Hangman.Game
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end
def init(_) do
{:ok, Game.new_game()}
end
def make_move(pid, guess) do
GenServer.call(pid, {:make_move, guess})
end
#########
def handle_call({:make_move, guess}, _from, game) do
Game.make_move(game, guess)
|> reply_call()
end
def handle_call({:tally}, _from, game) do
{game, Game.tally(game)}
|> reply_call()
end
defp reply_call({game, tally}) do
{:reply, tally, game}
end
end
# hangman/game.ex
defmodule Hangman.Game do
defstruct(
turns_left: 7,
game_state: :initializing,
letters: [],
used: MapSet.new()
)
def new_game(word) do
%Hangman.Game{
letters: word |> String.codepoints()
}
end
def new_game() do
new_game(Dictionary.random_word())
end
def make_move(game = %{game_state: state}, _guess) when state in [:won, :lost] do
return_with_tally(game)
end
def make_move(game, guess) do
accept_move(game, guess, MapSet.member?(game.used, guess))
|> return_with_tally()
end
def tally(game) do
%{
game_state: game.game_state,
turns_left: game.turns_left,
letters: game.letters |> reveal_guessed(game.used)
}
end
#############################################################################
defp accept_move(game, _guess, _already_guessed = true) do
Map.put(game, :game_state, :already_used)
end
defp accept_move(game, guess, _already_guessed) do
Map.put(game, :used, MapSet.put(game.used, guess))
|> score_guess(Enum.member?(game.letters, guess))
end
defp score_guess(game, _good_guess = true) do
new_state =
MapSet.new(game.letters)
|> MapSet.subset?(game.used)
|> maybe_won()
Map.put(game, :game_state, new_state)
end
defp score_guess(game = %{turns_left: 1}, _not_good_guess = false) do
Map.put(game, :game_state, :lost)
end
defp score_guess(game = %{turns_left: turns_left}, _not_good_guess = false) do
%{
game
| game_state: :bad_guess,
turns_left: turns_left - 1
}
end
defp reveal_guessed(letters, used) do
letters
|> Enum.map(fn letter -> reveal_letter(letter, MapSet.member?(used, letter)) end)
end
defp reveal_letter(letter, _in_word = true), do: letter
defp reveal_letter(_letter, _not_in_word), do: "_"
defp maybe_won(true), do: :won
defp maybe_won(_), do: :good_guess
defp return_with_tally(game), do: {game, tally(game)}
end