Several people have questioned my description of when you should use
dynamic components:

Does your component maintain state across multiple calls, and do you
need multiple versions of that state? For example, are you
representing a user session, or the state of games being played? If
so, use a dynamic component, where each component maintains state for
the session/game/….

Their objection is that I need to add the word shared, so that you
only need dynamic components to share data.

It’s an interesting point of view, and it took me a while to work out
their reasoning. Now that I understand it, I’m still sticking my my
statement, but I’d like to explain both points of view.

Recap: Dynamic Components

A dynamic component represents a server factory. You ask the factory for
a new server, and then use that server until you’re done with it. Being
a server, it can maintain its own state.

Let’s Use Hangman as an Example

Say you’re implementing a game of hangman.

We’ll agree up front to make the game a separate component: that is,
we’ll do

and write the game logic in this new directory tree.

We’ll write various clients for the game (a command line line, a Phoenix
client, and so on).

The game itself has state (the word to be guessed, letters guessed so
far, and so on). It also has an API that reveals an external version of
that state.

Each game being played needs its own state.

The debate is about where that state should be held.

Their Perspective on This

Some folks argue that each client has exactly one game. Although the
client will be a process, there’s no need for the game to be one: the
client can hold the game’s state, passing it in on each call to the game
API and updating it on each return from the API. The client code might look
something like this:

def play_game() do
  game = %Hangman.State{} |> Hangman.choose_word_to_guess

def accept_guess(game) do
  IO.puts "The word so far: #{game.word_so_far}"
  IO.puts "Letters used: #{game.letters_guessed |> Enum.join(",")}"
  guess = get_next_letter()
  game  = Hangman.score_guess(game, guess)
  cond do
   game.won || game.lost ->
   game.good_guess ->
    handle_good_guess(game, guess)
    handle_bad_guess(game, guess)


My Perspective

I’m really nervous about the client having unfettered
access to the state of the game. I’m not worried about cheating. I’m
concerned because the state is really to do with the implementation of
the game. And the implementation of a module should be no ones business
but the module’s.

In the code above, the client assumes that the game state contains a
string, word_so_far representing the current state of the guess, and
that it contains an enumerable, letters_guessed of the guesses so far.

Maybe when we first wrote it, both were true.

But maybe things changed in the game. We decided to change the
way we record guesses—say we use a bitmap instead. And maybe we no
longer keep a version of the word so far: we can always reconstruct it
from the target word and the guesses.

Both of these things are internal implementation issues, but both
changes break the client.

This is coupling, and on a larger scale this is a major reason changing
code is difficult,

If processes in Elixir were expensive, I would 100% agree with this
approach. But they’re not. Instead, developer time is a significant
cost, and in particular the time spend maintaining and changing code. So
I’m prepared to take the hit of having an extra process lying around if
it makes the life of future-me easier.

So my approach would be to turn the game from being as library into
being a server. It keeps its game state totally hidden from the outside
world: all the rest of the world gets to see is a PID and an API.

The client might look something like this:

alias Hangman, as: H  # 'cos I'm lazy

def play_game() do
  game = H.create()

def accept_guess(game) do
  IO.puts "The word so far: #{H.word_so_far(game)}"
  IO.puts "Letters used: #{H.letters_guessed(game) |> Enum.join(",")}"
  guess = get_next_letter()
  result = Hangman.score_guess(game)
  cond do
   result.won || result.lost ->
   result.good_guess ->
    handle_good_guess(game, guess)
    handle_bad_guess(game, guess)

Not much different, really. But now the internal state is opaque, and
it’s only made available via API calls. If we change the internal
implementation, we can keep the same API, and therefore not break

So, because of this, I’m a big fan of encapsulating state inside
processes, even if it isn’t technically necessary.

And that’s what I use Dynamic components for.

As always, I’m looking forward to some interesting comments and perspectives.

Dave Thomas

Source link