J'ai regardé le discours de Thuart Sierra " Thinking In Data " et en ai tiré l'une des idées comme principe de conception dans le jeu que je réalise. La différence est qu'il travaille à Clojure et je travaille en JavaScript. Je vois des différences majeures entre nos langues en ce sens:
- Clojure est une programmation idiomatiquement fonctionnelle
- La plupart des états sont immuables
J'ai repris l'idée de la diapositive "Tout est une carte" (de 11 minutes, 6 secondes à> 29 minutes). Certaines choses qu'il dit sont:
- Chaque fois que vous voyez une fonction qui prend 2 ou 3 arguments, vous pouvez justifier de la transformer en une carte et de ne la transmettre qu'à une carte. Cela présente de nombreux avantages:
- Vous n'avez pas à vous soucier de l'ordre des arguments
- Vous n'avez pas à vous soucier d'informations supplémentaires. S'il y a des clés supplémentaires, ce n'est pas vraiment notre préoccupation. Ils ne font que passer, ils n'interfèrent pas.
- Vous n'êtes pas obligé de définir un schéma
- Contrairement à la transmission d'un objet, aucune donnée ne se cache. Mais, il soutient que le masquage de données peut causer des problèmes et est surestimé:
- Performance
- Facilité de mise en œuvre
- Dès que vous communiquez sur le réseau ou entre les processus, vous devez de toute façon avoir un accord sur la représentation des données. C'est un travail supplémentaire que vous pouvez ignorer si vous travaillez uniquement sur des données.
Plus pertinent pour ma question. Cela fait 29 minutes: "Rendez vos fonctions composables". Voici l'exemple de code qu'il utilise pour expliquer le concept:
;; Bad (defn complex-process [] (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
Je comprends que la majorité des programmeurs ne connaissent pas Clojure, je vais donc réécrire ceci dans un style impératif:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Voici les avantages:
- Facile à tester
- Facile à regarder ces fonctions isolément
- Facile à commenter une ligne de cela et voir ce que le résultat est en supprimant une seule étape
- Chaque sous-processus pourrait ajouter plus d'informations à l'état. Si le sous-processus un doit communiquer quelque chose au sous-processus trois, il suffit simplement d'ajouter une clé / valeur.
- Pas de passe-partout pour extraire les données dont vous avez besoin hors de l'état juste pour pouvoir les sauvegarder. Passez simplement l'état entier et laissez le sous-processus affecter ce dont il a besoin.
Maintenant, revenons à ma situation: j'ai pris cette leçon et je l'ai appliquée à mon jeu. Autrement dit, presque toutes mes fonctions de haut niveau prennent et renvoient un gameState
objet. Cet objet contient toutes les données du jeu. EG: Une liste de badGuys, une liste de menus, le butin au sol, etc. Voici un exemple de ma fonction de mise à jour:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
Je suis ici pour vous demander si j'ai créé une abomination qui a perverti une idée qui n'est pratique que dans un langage de programmation fonctionnel. JavaScript n'est pas idiomatiquement fonctionnel (bien qu'il puisse être écrit de cette façon) et il est très difficile d'écrire des structures de données immuables. Une chose qui me préoccupe est qu’il suppose que chacun de ces sous-processus est pur. Pourquoi cette hypothèse doit-elle être faite? Il est rare que l' un de mes fonctions sont pures (par là, je veux dire qu'ils modifient souvent gameState
. Je n'ai pas d' autres effets secondaires complexes autres que). Ces idées s'effondrent-elles si vous ne disposez pas de données immuables?
Je crains qu'un jour je me réveille et réalise que toute cette conception est un simulacre et que je viens vraiment de mettre en œuvre l' anti-motif Big Ball Of Mud .
Honnêtement, je travaille sur ce code depuis des mois et c’est génial. Je sens que je reçois tous les avantages qu'il a revendiqués. Mon code est très facile pour moi de raisonner. Mais je suis un one man team alors j'ai la malédiction de la connaissance.
Mise à jour
Je code depuis plus de 6 mois avec ce modèle. Habituellement, à ce moment-là, j'oublie ce que j'ai fait et c'est là que "ai-je écrit cela d'une manière propre?" entre en jeu. Si je ne l'avais pas fait, je me battrais vraiment. Jusqu'à présent, je ne me bats pas du tout.
Je comprends combien d’autres yeux seraient nécessaires pour valider sa maintenabilité. Tout ce que je peux dire, c'est que je tiens avant tout à la facilité d'entretien. Je suis toujours l'évangéliste le plus bruyant pour le code propre, peu importe où je travaille.
Je souhaite répondre directement à ceux qui ont déjà une mauvaise expérience personnelle avec cette méthode de codage. Je ne le savais pas à l'époque, mais je pense que nous parlons en réalité de deux manières différentes d'écrire du code. La façon dont je l'ai fait semble être plus structurée que ce que d'autres ont connu. Quand quelqu'un a une mauvaise expérience personnelle avec "Tout est une carte", il explique à quel point il est difficile à maintenir parce que:
- Vous ne connaissez jamais la structure de la carte requise par la fonction
- N'importe quelle fonction peut transformer l'entrée de manière inattendue. Vous devez parcourir la base de code pour savoir comment une clé particulière est entrée dans la carte ou pourquoi elle a disparu.
Pour ceux qui ont une telle expérience, la base de code était peut-être «Tout prend 1 des N types de cartes». Le mien est, "Tout prend 1 de 1 type de carte". Si vous connaissez la structure de ce type, vous connaissez la structure de tout. Bien entendu, cette structure se développe généralement avec le temps. C'est pourquoi...
Il y a un endroit où rechercher l'implémentation de référence (c'est-à-dire: le schéma). Cette implémentation de référence est le code utilisé par le jeu pour ne pas devenir obsolète.
En ce qui concerne le deuxième point, je n’ajoute / retire pas de clés à la carte en dehors de l’implémentation de référence, je mute simplement ce qui est déjà là. J'ai aussi une grande suite de tests automatisés.
Si cette architecture finit par s'effondrer sous son propre poids, j'ajouterai une deuxième mise à jour. Sinon, supposons que tout se passe bien :)