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 :)
la source
Réponses:
J'ai déjà supporté une application dans laquelle "tout est une carte" auparavant. C'est une idée terrible. SVP ne le faites pas!
Lorsque vous spécifiez les arguments passés à la fonction, il est très facile de savoir quelles valeurs ont besoin de la fonction. Cela évite de transmettre des données superflues à la fonction qui distrait simplement le programmeur - chaque valeur transmise implique la nécessité de le faire, ce qui oblige le programmeur prenant en charge votre code à comprendre pourquoi les données sont nécessaires.
D'autre part, si vous transmettez tout en tant que carte, le programmeur prenant en charge votre application devra comprendre parfaitement la fonction appelée de toutes les manières pour savoir quelles valeurs la carte doit contenir. Pire encore, il est très tentant de réutiliser la carte transmise à la fonction actuelle afin de transmettre des données aux fonctions suivantes. Cela signifie que le programmeur prenant en charge votre application doit connaître toutes les fonctions appelées par la fonction actuelle afin de comprendre ce que fait la fonction actuelle. C’est exactement le contraire de l’objectif de l’écriture de fonctions - résumer les problèmes pour ne pas avoir à y penser! Maintenant, imaginez 5 appels en profondeur et 5 appels en largeur chacun. C'est beaucoup de choses à garder à l'esprit et beaucoup d'erreurs à faire.
"Tout est une carte" semble également conduire à utiliser la carte comme valeur de retour. Je l'ai vu. Et encore une fois, c'est pénible. Les fonctions appelées ne doivent jamais écraser la valeur de retour de l'autre - à moins de connaître la fonctionnalité de tout et de savoir que la valeur de mappage en entrée X doit être remplacée pour le prochain appel à une fonction. Et la fonction actuelle a besoin de modifier la carte pour renvoyer sa valeur, qui doit parfois écraser la valeur précédente et parfois non.
edit - exemple
Voici un exemple de situation problématique. C'était une application web. Les entrées utilisateur ont été acceptées à partir de la couche d'interface utilisateur et placées dans une carte. Ensuite, des fonctions ont été appelées pour traiter la demande. Le premier ensemble de fonctions vérifiera les entrées erronées. S'il y avait une erreur, le message d'erreur serait placé dans la carte. La fonction appelante vérifie la carte pour cette entrée et écrit la valeur dans l'interface utilisateur si elle existait.
L'ensemble de fonctions suivant démarrera la logique métier. Chaque fonction prendrait la carte, enlèverait des données, modifierait certaines données, agirait sur les données de la carte et placerait le résultat dans la carte, etc. Afin de corriger un bogue dans une fonction ultérieure, vous deviez examiner toutes les fonctions précédentes ainsi que l'appelant afin de déterminer où la valeur attendue aurait pu être définie.
Les fonctions suivantes extrairaient des données de la base de données. Ou plutôt, ils transmettraient la carte à la couche d'accès aux données. Le DAL vérifierait si la carte contenait certaines valeurs pour contrôler le mode d'exécution de la requête. Si 'justcount' était une clé, alors la requête serait 'compter select foo from bar'. N'importe laquelle des fonctions précédemment appelées aurait pu être celle qui a ajouté 'justcount' à la carte. Les résultats de la requête seraient ajoutés à la même carte.
Les résultats apparaîtront à l'appelant (logique métier) qui vérifiera la carte pour savoir quoi faire. Cela provient en partie d'éléments ajoutés à la carte par la logique métier initiale. Certains proviendraient des données de la base de données. La seule façon de savoir d'où cela venait était de trouver le code qui l'a ajouté. Et l'autre endroit qui peut aussi l'ajouter.
Le code était en réalité un gâchis monolithique qu'il fallait comprendre dans son intégralité pour savoir d'où venait une seule entrée sur la carte.
la source
gameState
sans savoir quoi que ce soit avant ou après. Il réagit simplement aux données fournies. Comment êtes-vous arrivé à une situation où les fonctions se mettraient sur les pieds? Pouvez-vous donner un exemple?Personnellement, je ne recommanderais pas ce modèle dans l'un ou l'autre paradigme. Cela facilite l'écriture initiale au lieu de rendre plus difficile de raisonner plus tard.
Par exemple, essayez de répondre aux questions suivantes sur chaque fonction de sous-processus:
state
t-il besoin?Avec ce modèle, vous ne pouvez pas répondre à ces questions sans lire l'intégralité de la fonction.
Dans un langage orienté objet, le motif a encore moins de sens, car l’état de suivi est ce que les objets font.
la source
Ce que vous semblez faire est en réalité une monade d’État manuelle; ce que je voudrais faire est de construire un combinateur de liaisons (simplifié) et de ré-exprimer les connexions entre vos étapes logiques en utilisant ceci:
Vous pouvez même utiliser
stateBind
pour construire les différents sous-processus à partir de sous-processus et poursuivre dans une arborescence de combinateurs de liaison pour structurer votre calcul de manière appropriée.Pour une explication de la monade d'État complète et non simplifiée et une excellente introduction à la monade en général en JavaScript, consultez ce billet de blog .
la source
stateBind
combinateur que j'ai donné en est un exemple très simple.Donc, il semble y avoir beaucoup de discussions sur l'efficacité de cette approche dans Clojure. Je pense qu'il pourrait être utile d'examiner la philosophie de Rich Hickey pour expliquer pourquoi il a créé Clojure afin de prendre en charge les abstractions de données de la manière suivante :
Fogus réitère ces points dans son livre Javascript fonctionnel :
la source
l'avocat du Diable
Je pense que cette question mérite l'avocat du diable (mais bien sûr, je suis partial). Je pense que @KarlBielefeldt avance de très bons arguments et j'aimerais y répondre. Je veux d’abord dire que ses arguments sont excellents.
Comme il a mentionné que ce modèle ne convient pas, même en programmation fonctionnelle, je prendrai en compte JavaScript et / ou Clojure dans mes réponses. Une similitude extrêmement importante entre ces deux langues est leur typage dynamique. Je serais plus d'accord avec ses remarques si je mettais cela en œuvre dans un langage typé statiquement comme Java ou Haskell. Mais je vais considérer que l’alternative au motif "Tout est une carte" est une conception POO traditionnelle en JavaScript et non dans un langage typé de manière statique (j’espère que je ne mets pas en place un argument de type homme de paille en faisant cela, s'il vous plaît, faites-moi savoir).
Dans une langue à typage dynamique, comment répondriez-vous normalement à ces questions? Le premier paramètre d'une fonction peut être nommé
foo
, mais qu'est-ce que c'est? Un tableau? Un objet? Un objet de tableaux d'objets? Comment trouvez-vous? Le seul moyen que je connaisse est deJe ne pense pas que le motif "Tout est une carte" fait une différence ici. Ce sont toujours les seuls moyens que je connaisse pour répondre à ces questions.
N'oubliez pas non plus qu'en JavaScript et dans les langages de programmation les plus impératifs, n'importe qui
function
peut exiger, modifier ou ignorer tout état auquel il peut accéder et que la signature ne fait aucune différence: la fonction / méthode peut faire quelque chose avec l'état global ou avec un singleton. Les signatures mentent souvent .Je n'essaie pas de créer une fausse dichotomie entre "Tout est une carte" et un code OO mal conçu . J'essaie simplement de souligner que le fait d'avoir des signatures qui prennent moins / plus de paramètres à granularité grossière / grossière ne garantit pas que vous savez comment isoler, configurer et appeler une fonction.
Mais, si vous me permettez d’utiliser cette fausse dichotomie: comparé à l’écriture de JavaScript à la manière traditionnelle de la programmation orientée objet, "Tout est une carte" semble mieux. Selon la méthode OOP traditionnelle, la fonction peut nécessiter, modifier ou ignorer l’état que vous transmettez ou indiquez que vous ne le transmettez pas. dans.
Dans mon code, oui. Voir mon deuxième commentaire à la réponse de @ Evicatos. Peut-être que c'est uniquement parce que je fais un jeu, je ne peux pas le dire. Dans un jeu qui est mise à jour 60x une seconde, il n'a pas vraiment d' importance si
dead guys drop loot
alors ,good guys pick up loot
ou vice versa. Chaque fonction fait toujours exactement ce qu'elle est supposée faire, quel que soit l'ordre dans lequel elle est exécutée. Les mêmes données sont simplement introduites dans cesupdate
appels lors de différents appels si vous échangez la commande. Si vous avezgood guys pick up loot
alorsdead guys drop loot
, les bons gars vont ramasser le butin dans la prochaineupdate
et ce n'est pas grave. Un humain ne sera pas capable de remarquer la différence.Au moins, cela a été mon expérience générale. Je me sens vraiment vulnérable d'admettre cela publiquement. Peut-être que considérer que tout va bien est une très , très mauvaise chose à faire. Faites-moi savoir si j'ai commis une terrible erreur ici. Mais, si je l’ai fait, il est extrêmement facile de réorganiser les fonctions, l’ordre est
dead guys drop loot
donc àgood guys pick up loot
nouveau. Cela prendra moins de temps que le temps nécessaire pour écrire ce paragraphe: PPeut-être pensez-vous que "les morts devraient laisser tomber le butin en premier. Ce serait mieux si votre code faisait respecter cet ordre". Mais, pourquoi les ennemis devraient-ils laisser tomber leur butin avant que vous puissiez le récupérer? Pour moi, cela n'a pas de sens. Peut-être que le butin a été largué
updates
il y a 100 ans. Il n'est pas nécessaire de vérifier si un méchant arbitraire doit récupérer un butin déjà au sol. C'est pourquoi je pense que l'ordre de ces opérations est complètement arbitraire.Il est naturel d'écrire des étapes découplées avec ce modèle, mais il est difficile de remarquer vos étapes couplées dans la POO traditionnelle. Si j’écrivais de la POO traditionnelle, la façon naturelle et naïve de penser est de faire du
dead guys drop loot
retour unLoot
objet que je dois transmettre à lagood guys pick up loot
. Je ne serais pas en mesure de réorganiser ces opérations car le premier retourne l'entrée de second.Les objets ont un état et il est idiomatique de le modifier, de sorte que son histoire disparaisse ... sauf si vous écrivez manuellement du code pour le suivre. En quoi l'état de suivi est-il "ce qu'ils font"?
Comme je l’ai dit, "c’est rare que mes fonctions soient pures". Ils opèrent toujours uniquement sur leurs paramètres, mais ils modifient leurs paramètres. C’est un compromis que j’ai estimé devoir faire lors de l’application de ce modèle à JavaScript.
la source
parent
? Est-ce querepetitions
et un tableau de nombres ou de chaînes ou est-ce pas important? Ou peut-être que les répétitions sont juste un nombre représentant le nombre de répétitions que je veux? Il y a beaucoup d'apis qui prennent juste un objet options . Le monde est meilleur si vous nommez les choses correctement, mais cela ne garantit pas que vous saurez utiliser l’API, sans poser de questions.J'ai trouvé que mon code a tendance à être structuré comme suit:
Je n'ai pas cherché à créer cette distinction, mais c'est souvent ainsi que cela se retrouve dans mon code. Je ne pense pas que l'utilisation d'un style annule nécessairement l'autre.
Les fonctions pures sont faciles à tester. Les cartes plus grandes avec des cartes entrent davantage dans la zone de test "d'intégration" car elles impliquent généralement plus de pièces mobiles.
En javascript, une chose qui aide beaucoup est d'utiliser quelque chose comme la bibliothèque Match de Meteor pour effectuer la validation des paramètres. Cela indique très clairement ce que la fonction attend et peut gérer les cartes de manière très propre.
Par exemple,
Voir http://docs.meteor.com/#match pour plus d'informations.
:: MISE À JOUR ::
L'enregistrement vidéo de "Clojure in the Large" de Clojure / West par Stuart Sierra aborde également ce sujet. Comme le PO, il contrôle les effets secondaires dans le cadre de la carte, ce qui facilite grandement les tests. Il a également publié un article dans son blog décrivant son flux de travail actuel pour Clojure qui semble pertinent.
la source
L'argument principal que je peux opposer à cette pratique est qu'il est très difficile de déterminer les données dont une fonction a réellement besoin.
Cela signifie que les futurs programmeurs de la base de code devront savoir comment la fonction appelée fonctionne en interne - et tous les appels de fonctions imbriquées - pour pouvoir l'appeler.
Plus j'y pense, plus votre objet gameState a une odeur de global. Si c'est comme ça qu'on l'utilise, pourquoi le faire circuler?
la source
gameState = f(gameState)
de modef()
, c'est beaucoup plus difficile à testerf
.f()
peut renvoyer une chose différente à chaque fois que je l'appelle. Mais il est facile de faire en sorte quef(gameState)
la même chose soit renvoyée à chaque fois.Il y a plus de nom qui convient à ce que vous faites que Big ball of boue . Ce que vous faites s'appelle le modèle d' objet de Dieu . Cela n’apparaît pas comme ça au premier abord, mais en Javascript il y a très peu de différence entre
et
Que ce soit ou non une bonne idée dépend probablement des circonstances. Mais cela va certainement dans le sens de la méthode Clojure. L'un des objectifs de Clojure est d'éliminer ce que Rich Hickey appelle "complexité incidente". La communication de plusieurs objets est certainement plus complexe qu’un seul objet. Si vous divisez la fonctionnalité en plusieurs objets, vous devez soudainement vous soucier de la communication, de la coordination et du partage des responsabilités. Ce sont des complications qui sont seulement accessoires à votre objectif initial d'écrire un programme. Vous devriez voir le discours de Rich Hickey dans Simple rendu facile . Je pense que c'est une très bonne idée.
la source
Je viens juste de faire face à ce sujet plus tôt dans la journée en jouant avec un nouveau projet. Je travaille à Clojure pour faire un jeu de poker. J'ai représenté les valeurs faciales et les costumes en tant que mots-clés et j'ai décidé de représenter une carte sous la forme d'une carte semblable à
J'aurais aussi bien pu en faire des listes ou des vecteurs des deux éléments de mots clés. Je ne sais pas si cela fait une différence entre la mémoire et les performances, je vais donc simplement utiliser des cartes pour le moment.
Dans le cas où je changerais d'avis par la suite, j'ai décidé que la plupart des parties de mon programme devaient passer par une "interface" pour accéder aux éléments d'une carte, de sorte que les détails de la mise en œuvre soient contrôlés et masqués. J'ai des fonctions
que le reste du programme utilise. Les cartes sont transmises aux fonctions sous forme de cartes, mais les fonctions utilisent une interface convenue pour accéder aux cartes et ne devraient donc pas pouvoir se gâcher.
Dans mon programme, une carte ne sera probablement jamais qu'une carte à deux valeurs. Dans la question, tout l'état du jeu est transmis sous forme de carte. L'état du jeu sera beaucoup plus compliqué qu'une seule carte, mais je ne pense pas qu'il soit fautif de recourir à une carte. Dans un langage impératif pour les objets, je pourrais tout aussi bien avoir un seul et même grand objet GameState et appeler ses méthodes, et avoir le même problème:
Maintenant, c'est orienté objet. Y a-t-il quelque chose de particulièrement faux avec cela? Je ne pense pas, vous déléguez simplement le travail à des fonctions qui savent comment gérer un objet State. Et que vous travailliez avec des cartes ou des objets, vous devriez vous méfier du moment de les séparer en morceaux plus petits. Je dis donc que l’utilisation de cartes convient parfaitement, à condition que vous utilisiez le même soin que celui que vous utiliseriez avec des objets.
la source
D'après ce que j'ai (peu) vu, utiliser des cartes ou d'autres structures imbriquées pour créer un seul objet d'état global immuable, comme celui-ci, est assez courant dans les langages fonctionnels, du moins dans les langages purs, en particulier lorsque State Monad est utilisé comme @Ptharien'sFlame mentioend .
Voici deux obstacles à l’utilisation efficace de ce contenu que j'ai vus / lus (et d’autres réponses mentionnées ici):
Il existe plusieurs techniques / modèles courants qui peuvent aider à résoudre ces problèmes:
Le premier est Zippers : ils permettent de traverser et de muter au plus profond d'une hiérarchie imbriquée immuable.
Une autre est Lenses : elles vous permettent de vous focaliser sur la structure vers un emplacement spécifique et de lire / modifier la valeur à cet endroit. Vous pouvez combiner différentes lentilles pour vous concentrer sur différentes choses, un peu comme une chaîne de propriétés ajustable dans la POO (où vous pouvez substituer des variables aux noms de propriété réels!)
Prismatic a récemment publié un blog sur l'utilisation de ce type de technique, entre autres choses, en JavaScript / ClojureScript, que vous devriez vérifier. Ils utilisent les curseurs (qu'ils comparent aux fermetures à glissière) pour définir l'état de la fenêtre:
IIRC, ils abordent également l’immuabilité en JavaScript dans cet article.
la source
assoc-in
et al). Je suppose que je viens d'avoir Haskell sur le cerveau. Je me demande si quelqu'un en a fait un portage JavaScript? Les gens ne l'ont probablement pas abordé parce que (comme moi) ils n'ont pas regardé la conversation :)Que ce soit une bonne idée ou non dépendra vraiment de ce que vous faites avec l'état à l'intérieur de ces sous-processus. Si je comprends bien l’exemple de Clojure, les dictionnaires d’État renvoyés ne sont pas les mêmes dictionnaires d’état. Ils sont des copies, éventuellement avec ajouts et modifications, que (je suppose) que Clojure est capable de créer efficacement car La nature fonctionnelle de la langue en dépend. Les dictionnaires d'état d'origine de chaque fonction ne sont en aucun cas modifiés.
Si je comprends bien, vous êtes en train de modifier les objets d'état que vous passez dans vos fonctions javascript plutôt que de retourner une copie, ce qui signifie que vous faites quelque chose de très, très différent de ce que le code Clojure fait. Comme Mike Partridge l’a souligné, il s’agit en gros d’une action globale que vous transmettez et que vous quittez explicitement de fonctions sans raison réelle. À ce stade, je pense que cela vous fait simplement penser que vous faites quelque chose que vous n'êtes pas réellement.
Si vous faites explicitement des copies de l'état, que vous le modifiez, puis que vous retournez cette copie modifiée, continuez. Je ne suis pas sûr que ce soit nécessairement le meilleur moyen d'accomplir ce que vous essayez de faire en Javascript, mais c'est probablement "en gros plan" par rapport à ce que fait cet exemple de Clojure.
la source
update()
fonction. J'ai déplacé l'un vers le haut et l'autre vers le bas. Tous mes tests étaient toujours réussis et lorsque je jouais à mon jeu, je ne remarquais aucun effet néfaste. Je sens mes fonctions sont tout aussi composable que l'exemple Clojure. Nous jetons tous les deux nos anciennes données après chaque étape.(-> 10 (- 5) (/ 2))
retourne 2.5.(-> 10 (/ 2) (- 5))
renvoie 0.Si vous avez un objet d'état global, parfois appelé "objet divin", qui est transmis à chaque processus, vous finissez par confondre un certain nombre de facteurs, qui augmentent tous le couplage tout en diminuant la cohésion. Ces facteurs ont tous un impact négatif sur la maintenabilité à long terme.
Couplage de trampes Ceci résulte du transfert de données à travers différentes méthodes qui n'ont pas besoin de presque toutes les données, afin de les acheminer à l'endroit où elles peuvent réellement être traitées. Ce type de couplage s'apparente à l'utilisation de données globales, mais peut être plus contenu. Le couplage de tramp est l'opposé de "besoin de savoir", qui est utilisé pour localiser les effets et pour contenir les dommages qu'un code erroné peut avoir sur l'ensemble du système.
Navigation dans les données Chaque sous-processus de votre exemple doit savoir comment obtenir exactement les données dont il a besoin, et doit pouvoir le traiter et éventuellement construire un nouvel objet d'état global. C’est la conséquence logique du couplage tramp; l'intégralité du contexte d'une donnée est nécessaire pour pouvoir fonctionner sur la donnée. Encore une fois, les connaissances non locales sont une mauvaise chose.
Si vous passez dans un "zipper", un "objectif" ou un "curseur", comme décrit dans le message de @paul, c'est une chose. Vous seriez en train de contenir l'accès et d'autoriser la fermeture à glissière, etc., à contrôler la lecture et l'écriture des données.
Violation de responsabilité unique Revendiquer chacun des "sous-processus-un", "sous-processus-deux" et "sous-processus-trois" n'a qu'une seule responsabilité, à savoir produire un nouvel objet d'état global avec les bonnes valeurs, est un réductionnisme flagrant. C'est tout à la fin, n'est-ce pas?
Ce que je veux dire ici, c'est qu'avoir toutes les principales composantes de votre jeu a les mêmes responsabilités, car votre jeu va à l'encontre de l'objectif de la délégation et de l'affacturage.
Impact des systèmes
L'impact majeur de votre conception est la faible maintenabilité. Le fait que vous puissiez garder le jeu entier dans votre tête signifie que vous êtes très probablement un excellent programmeur. J'ai conçu beaucoup de choses que je pourrais garder en tête pendant tout le projet. Ce n’est cependant pas le but de l’ingénierie des systèmes. Le but est de créer un système qui fonctionne pour quelque chose de plus grand qu'une personne peut garder dans sa tête en même temps .
Ajouter un autre programmateur, ou deux, ou huit, fera que votre système se désagrégera presque immédiatement.
finalement
la source