Enfiler des process comme des pâtes dans un collier de nouilles
Rappel de l'épisode précédent
Mon article précédent sur Elixir documentait le passage d'une modélisation de classe en C++ en une modélisation "objet" composée par un module et animé processus GenServer.
Pour créer des logiciels complexes, il s'agit maintenant d'assembler ces "objets" entre eux. De façon ultime, ils s'agit de pouvoir utiliser les patrons de conceptions (design patterns en bon français) en Elixir. Avant d'en arriver là, étudions les relations de bases entre "objets" et la façon de les implémenter en Elixir.
Les relations de bases
La composition
La composition d'un objet avec un ou plusieurs autres objets consiste à inclure les objets composés comme membres de l'objet chapeau et d'utiliser les fonctions des sous-objets pour contribuer à l'implémentation des fonctions de l'objet.
On crée l'objet composeur et dans la fonction init du module en question, on créé les sous-objets. Cette création des sous-objets fils se fait à l'aide de
GenServer.start_link()
ce qui permet de garder un lien entre le processus courant (le processus qui anime l'objet) et les processus fils. Il suffit ensuite de stocker les pid des processus fils dans la map d'état (contexte) du GenServer.
Si un processus fils s'arrête, le processus père reçoit un signal d'arrêt et si ce signal n'est pas traité, il est arrêté. De même, si le processus père qui anime l'objet principal s'arrête, les fils reçoivent un signal d'arrêt et par défaut libèrent leur ressource et s'arrêtent.
On a bien là les caractéristique d'une composition d'objet.
La référence
Un objet peut également utiliser un autre objet par référence. L'équivalent en Elixir n'est pas évident. Bien entendu, un processus Elixir peut stocker dans une variable d'état les PID d'autres processus mais il n'a pas de garantie que ce PID corresponde à un processus actif.
On peut vérifier cela en utilisant Kernel.alive?() mais ce n'est pas la meilleur option. Il est préférable d'enregistrer les processus à référencer avec des noms
Extrait de la doc de GenServer.start_link()
Name registration
Both start_link/3 and start/3 support the GenServer to register a name on start via the :name option. Registered names are also automatically cleaned up on termination. The supported values are:
Recommandé par LinkedIn
On peut alors utiliser alors les noms enregistrés ou une registry pour interagir avec le processus et s'assurer qu'il existe bien.
L'héritage
C'est la m ... en Elixir. Le langage n'est pas vraiment fait pour faire de héritage. On va redécouper ce besoin en deux :
La notion d'interface ou d'objet présentant une série de fonctions précises à implémenter est supporté par les behaviours. On peut définir un modèle de module comme ceci
defmodule Animal do
@doc "Definit le comportement d'un animal"
@callback nom(animal :: map() ) :: { atom(), binary() }
@callback mange(animal :: map(), nourriture :: atom ) :: { :ok, map() } | :boude
end
On utilise ensuite le modèle comme suit :
defmodule Chat do
@behaviour Animal
defp viande?(nourriture) do
nourriture in [ :volaille, :boeuf, :cochon ]
end
@impl
def nom(animal) when is_map(animal) and animal.type == :chat do
{ :chat, animal.nom }
endif
@impl
def mange(animal, nourriture) do
if viande?(nourriture) do
{ :ok, Map.put(animal, :etat, :repus) }
else
:boude
end
end
end
Ce n'est pas très utilisable parce qu'il faut connaitre le module à utiliser pour appliquer la bonne fonction. Si l'on veut manipuler des objets sans savoir de quel type ils sont alors il faut complexifier notre code.
On crée un wrapper qui interroge un genserver
defmodule Animal do
@doc "Definit le comportement d'un animal"
def nom(animal) when is_pid(animal) do
GenServer.call(animal, :nom)
end
def mange(animal, nourriture) when is_pid(animal) do
GenServer.call(animal, { :mange, nourriture } )
end
Ca n'est pas très élégant mais c'est la meilleure façon que j'ai trouvé de manipuler des objets sans exposer leur implémentation.
Extension d'un objet
On touche ici les limites de la transposition de la POO à Elixir. Il n'y a pas vraiment de mécanismes propre. On peut rassembler le code de la classe de base dans un module puis définir les fonction de la classe dérivée dans un autre module en emballant les appels du module de base dans le module dérivé. C'est moche.