Comment utiliser Control Inversion en JavaScript et Reactjs pour simplifier la gestion du code

Comment utiliser Control Inversion en JavaScript et Reactjs pour simplifier la gestion du code


Inversion of Control est un principe de programmation assez facile à comprendre qui, en même temps, peut considérablement améliorer votre code. Cet article montrera comment appliquer Control Inversion en JavaScript et dans Reactjs.


Si vous avez déjà écrit du code utilisé à plusieurs endroits, vous connaissez cette situation:


  1. Vous créez un morceau de code réutilisable (il peut s'agir d'une fonction, d'un composant React, d'un hook React, etc.) et le partagez (pour la collaboration ou la publication en open source).
  2. Quelqu'un vous demande d'ajouter de nouvelles fonctionnalités. Votre code ne prend pas en charge la fonctionnalité proposée, mais il pourrait le faire si vous apportiez une petite modification.
  3. Vous ajoutez un nouvel argument / prop / option à votre code et à sa logique associée pour que cette nouvelle fonctionnalité continue de fonctionner.
  4. Répétez les étapes 2 et 3 plusieurs fois (ou plusieurs fois).
  5. Maintenant, votre code réutilisable est difficile à utiliser et à maintenir.

Qu'est-ce qui fait du code un cauchemar à utiliser et à maintenir? Plusieurs aspects peuvent rendre votre code problématique:


  1. Taille et / ou performances du package: un peu plus de code à exécuter sur les appareils peut entraîner de mauvaises performances. Parfois, cela peut conduire à des personnes refusant simplement d'utiliser votre code.
  2. Difficile à maintenir: Auparavant, votre code réutilisable n'avait que quelques options, et il était axé sur une bonne chose, mais maintenant il peut faire un tas de choses différentes, et vous devez tout documenter. De plus, les gens commenceront à vous poser des questions sur la façon d'utiliser votre code pour certains cas d'utilisation qui peuvent ou non être comparables aux cas d'utilisation pour lesquels vous avez déjà ajouté un support. Vous pouvez même avoir deux cas d'utilisation presque identiques qui sont légèrement différents, vous devrez donc répondre à des questions sur ce qui est le mieux à utiliser dans une situation donnée.
  3. Complexité de l'implémentation : chaque fois qu'il ne s'agit pas simplement d' une autre if , chaque branche de la logique de votre code coexiste avec les branches de logique existantes. En fait, des situations sont possibles lorsque vous essayez de conserver une combinaison d'arguments / options / accessoires que personne n'utilise même, mais vous devez toujours envisager toutes les options possibles, car vous ne savez pas exactement si quelqu'un utilisera ou non ces combinaisons.
  4. API sophistiquée : chaque nouvel argument / option / accessoire que vous ajoutez à votre code réutilisable le rend difficile à utiliser, car maintenant vous avez un énorme README ou un site où toutes les fonctionnalités disponibles sont documentées, et les gens doivent apprendre tout cela pour une utilisation efficace votre code. Son utilisation n'est pas pratique, car la complexité de votre API pénètre dans le code du développeur qui l'utilise, ce qui complique son code.

En conséquence, tout le monde souffre. Il convient de noter que la mise en œuvre du programme final est un élément essentiel du développement. Mais ce serait formidable si nous réfléchissions davantage à la mise en œuvre de nos abstractions (lire à propos de la "programmation AHA" ). Existe-t-il un moyen de réduire les problèmes liés au code réutilisable tout en profitant des avantages des abstractions?


Inversion de contrôle


L'inversion du contrôle est un principe qui simplifie vraiment la création et l'utilisation d'abstractions. Voici ce que Wikipedia en dit:


... dans la programmation traditionnelle, le code utilisateur qui exprime le but du programme est appelé dans des bibliothèques réutilisables pour résoudre les problèmes courants, mais avec l'inversion de contrôle, c'est un environnement qui appelle du code personnalisé ou spécifique à la tâche,

Pensez-y de cette façon: "Réduisez les fonctionnalités de votre abstraction et faites en sorte que vos utilisateurs puissent implémenter les fonctionnalités dont ils ont besoin." Cela peut sembler une absurdité complète, car nous utilisons des abstractions pour masquer des tâches complexes et répétitives, et ainsi rendre notre code plus «propre» et «soigné». Mais, comme nous l'avons vu plus haut, les abstractions traditionnelles ne simplifient pas toujours le code.


Qu'est-ce que l'inversion de la gestion dans le code?


Pour commencer, voici un exemple très artificiel:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

Jouons maintenant un «cycle de vie d'abstraction» typique en ajoutant de nouveaux cas d'utilisation à cette abstraction et en l '«améliorant sans réfléchir» pour prendre en charge ces nouveaux cas d'utilisation:


 //   Array.prototype.filter   function filter( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if ( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ) { continue } newArray[newArray.length] = element } return newArray } filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterNull: false}) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterUndefined: false}) // [0, 1, 2, undefined, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterZero: true}) // [1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterEmptyString: true}) // [0, 1, 2, 3, 'four'] 

Donc, notre programme ne fonctionne qu'avec six cas d'utilisation, mais en fait, nous prenons en charge toute combinaison de fonctions possible, et il y en a jusqu'à 25 (si j'ai calculé correctement).


En général, il s'agit d'une abstraction assez simple. Mais cela peut être simplifié. Il arrive souvent que l'abstraction dans laquelle de nouvelles fonctionnalités ont été ajoutées pourrait être considérablement simplifiée pour les cas d'utilisation qu'elle prend en charge. Malheureusement, dès que l'abstraction commence à prendre en charge quelque chose (par exemple, en exécutant { filterZero: true, filterUndefined: false } ), nous avons peur de supprimer cette fonctionnalité car elle risque de casser le code qui en dépend.


Nous écrivons même des tests pour des cas d'utilisation que nous n'avons pas réellement, simplement parce que notre abstraction prend en charge ces scénarios, et nous devrons peut-être le faire à l'avenir. Et lorsque ces cas d'utilisation ou d'autres deviennent inutiles pour nous, nous ne supprimons pas leur support, car nous l'oublions simplement, ou pensons qu'il peut nous être utile à l'avenir, ou simplement nous avons peur de casser quelque chose.


D'accord, écrivons maintenant une abstraction plus élaborée pour cette fonction et appliquons la méthode d'inversion de contrôle pour prendre en charge tous les cas d'utilisation dont nous avons besoin:


 //   Array.prototype.filter   function filter(array, filterFn) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (filterFn(element)) { newArray[newArray.length] = element } } return newArray } filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null) // [0, 1, 2, undefined, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== 0, ) // [1, 2, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== '', ) // [0, 1, 2, 3, 'four'] 

Super! Cela s'est avéré beaucoup plus facile. Nous venons juste de contrôler la fonction, en transférant la responsabilité de décider quel élément appartient au nouveau tableau, de la fonction de filter à la fonction qui appelle la fonction de filtre. Notez que la fonction de filter est toujours une abstraction utile en soi, mais maintenant elle est beaucoup plus flexible.


Mais la version précédente de cette abstraction était-elle si mauvaise? Probablement pas. Mais depuis que nous avons pris le contrôle, nous pouvons désormais prendre en charge des cas d'utilisation beaucoup plus uniques:


 filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Imaginez simplement si vous aviez besoin de prendre en charge ce cas d'utilisation sans appliquer d'inversion de contrôle? Oui, ce serait tout simplement ridicule.


Mauvaise API?


L'une des plaintes les plus courantes que j'entends des gens concernant les API qui utilisent l'inversion de contrôle est: "Oui, mais maintenant c'est plus difficile à utiliser qu'auparavant." Prenez cet exemple:


 //  filter([0, 1, undefined, 2, null, 3, 'four', '']) //  filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) 

Oui, l'une des options est clairement plus facile à utiliser que l'autre. Mais l'un des avantages de l'inversion de contrôle est que vous pouvez utiliser une API qui utilise l'inversion de contrôle pour réimplémenter votre ancienne API. C'est généralement assez simple. Par exemple:


 function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) } 

Cool, hein? De cette façon, nous pouvons créer des abstractions au-dessus de l'API dans laquelle l'inversion de contrôle est appliquée, et ainsi créer une API plus simple. Et si notre API "plus simple" n'a pas suffisamment de cas d'utilisation, alors nos utilisateurs peuvent appliquer les mêmes blocs de construction que nous avons utilisés pour créer notre API de haut niveau pour développer des solutions pour des tâches plus complexes. Ils n'ont pas besoin de nous demander d'ajouter une nouvelle fonction à filterWithOptions et d'attendre qu'elle soit implémentée. Ils disposent déjà d'outils avec lesquels ils peuvent développer indépendamment les fonctionnalités supplémentaires dont ils ont besoin.


Et, juste pour le fan:


 function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

Vous pouvez créer des fonctionnalités spéciales pour toute situation qui se produit souvent avec vous.


Exemples concrets


Donc, cela fonctionne dans des cas simples, mais ce concept convient-il à la vie réelle? Eh bien, vous utilisez probablement constamment l'inversion de contrôle. Par exemple, la fonction Array.prototype.filter applique un contrôle inverse. Comme la fonction Array.prototype.map .


Il existe différents modèles que vous connaissez peut-être déjà et qui ne sont qu'une forme d'inversion de contrôle.


Voici deux de mes modèles préférés qui en font la démonstration: «Composés composés» et «Réducteurs d'état» . Vous trouverez ci-dessous de brefs exemples d'application de ces modèles.


Composants composés


Supposons que vous vouliez créer un composant Menu qui possède un bouton pour ouvrir un menu et une liste d'éléments de menu qui seront affichés lorsque vous cliquez sur le bouton. Ensuite, lorsque l'élément est sélectionné, il effectuera une action. Habituellement, pour implémenter cela, ils créent simplement des accessoires:


 function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden></span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) } 

Cela nous permet de configurer beaucoup de choses dans les éléments de menu. Mais que se passe-t-il si nous voulons insérer une ligne avant l'élément de menu Supprimer? Faut-il ajouter une option spéciale aux objets liés aux items ? Eh bien, je ne sais pas, par exemple: precedeWithLine ? Idée moyenne.


Créez peut-être un type de menu spécial, par exemple {contents: <hr />} . Je pense que cela fonctionnerait, mais il nous faudrait alors gérer les cas où il n'y a pas d' onSelect . Et pour être honnête, il s'agit d'une API très maladroite.


Lorsque vous réfléchissez à la façon de créer une bonne API pour les personnes qui essaient de faire quelque chose d'un peu différemment, au lieu d'atteindre l'instruction if , essayez d'inverser le contrôle. Et si nous transférons la responsabilité de la visualisation des menus à l'utilisateur? Nous utilisons l'une des forces de la réaction:


 function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden></span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) } 

Un point important à prendre en compte est qu'il n'y a aucun état des composants visible par l'utilisateur. L'état est implicitement partagé entre ces composants. Il s'agit de la valeur fondamentale du modèle de composant. En utilisant cette opportunité, nous avons donné un certain contrôle sur le rendu à l'utilisateur de nos composants, et maintenant ajouter une ligne supplémentaire (ou quelque chose d'autre) est une action simple et intuitive. Pas de documentation supplémentaire, pas de fonctions supplémentaires, pas de code ou de tests supplémentaires. Tout le monde gagne.


Vous pouvez en savoir plus sur ce modèle ici . Merci à Ryan Florence , qui m'a appris cela.


Réducteur d'état


J'ai trouvé ce modèle pour résoudre le problème de la configuration de la logique des composants. Vous pouvez en savoir plus sur cette situation sur mon blog , The State Reducer Pattern , mais le point principal est que j'ai une bibliothèque de recherche / saisie semi-automatique / Downshift appelée Downshift , et l'un des utilisateurs de la bibliothèque développait une version de composant à choix multiples , à cause de quoi il voulait que le menu reste ouvert même après avoir sélectionné un élément.


La logique derrière Downshift suggéré qu'après le choix, le menu devrait se fermer. Un utilisateur de bibliothèque qui avait besoin de changer ses fonctionnalités a suggéré d'ajouter prop closeOnSelection . J'ai décliné cette offre, car une fois que j'avais déjà parcouru le chemin menant à un propalapse , je voulais l'éviter.


Au lieu de cela, j'ai créé l'API pour que les utilisateurs eux-mêmes puissent contrôler la façon dont les changements d'état se produisent. Considérez le réducteur d'état comme l'état d'une fonction qui est appelée à chaque fois que l'état du composant change, et donne au développeur d'application la possibilité d'influencer le changement d'état qui est sur le point de se produire.


Un exemple d'utilisation de la bibliothèque Downshift pour qu'elle ne ferme pas le menu après que l'utilisateur a cliqué sur l'élément sélectionné:


 function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes, //     Downshift   //       isOpen  highlightedIndex //      isOpen: state.isOpen, highlightedIndex: state.highlightedIndex, } default: return changes } } // ,   // <Downshift stateReducer={stateReducer} {...restOfTheProps} /> 

Après avoir ajouté cet accessoire, nous avons commencé à recevoir BEAUCOUP moins de demandes d'ajout de nouveaux paramètres pour ce composant. Le composant est devenu plus flexible et il est devenu plus facile pour les développeurs de le configurer selon leurs besoins.


Accessoires de rendu


Il convient de mentionner le modèle des «accessoires de rendu» . Ce modèle est un exemple idéal d'utilisation de l'inversion de contrôle, mais nous n'en avons surtout pas besoin. En savoir plus ici: pourquoi nous n'avons plus autant besoin de Render Props .


Avertissement


L'inversion du contrôle est un excellent moyen de contourner le problème des idées fausses sur la façon dont votre code sera utilisé à l'avenir. Mais avant de terminer, je voudrais vous donner quelques conseils.


Revenons à notre exemple tiré par les cheveux:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

Et si c'est tout ce dont nous avons besoin de la fonction de filter ? Et nous n'avons jamais fait face à une situation où nous aurions besoin de filtrer quoi que ce soit sauf null et undefined ? Dans ce cas, l'ajout d'une inversion de contrôle pour un cas d'utilisation unique compliquerait simplement le code et n'apporterait pas beaucoup d'avantages.


Comme pour toute abstraction, veillez à appliquer le principe de la programmation AHA et évitez les abstractions hâtives!


Conclusions


J'espère que l'article vous a été utile. J'ai montré comment appliquer le concept de Control Inversion dans une réaction. Ce concept, bien sûr, ne s'applique pas seulement à React (comme nous l'avons vu avec la fonction de filter ). La prochaine fois que vous remarquerez que vous ajoutez une autre if à la fonction coreBusinessLogic de votre application, réfléchissez à la façon dont vous pouvez inverser le contrôle et transférer la logique là où il est utilisé (ou, s'il est utilisé à plusieurs endroits, vous pouvez créer une abstraction plus spécialisée pour cela. cas particulier).


Si vous le souhaitez, vous pouvez jouer avec un exemple d'un article sur CodeSandbox .


Bonne chance et merci pour votre attention!


PS. Si vous avez aimé cet article, vous aimerez peut-être cet exposé: youtube Kent C Dodds - Simply React

Source: https://habr.com/ru/post/fr485012/


All Articles