Écrire des composants réutilisables en respectant SOLID

Bonjour à tous! Je m'appelle Roma, je suis un frontend dans Ya Tutorial. Aujourd'hui, je vais vous expliquer comment éviter la duplication de code et écrire des composants réutilisables de haute qualité. L'article a été écrit sur la base (mais uniquement pour des motifs !) du rapport de Y. Subbotnik - il y a une vidéo à la fin de l'article. Si vous êtes intéressé à comprendre ce sujet, bienvenue sous cat.



L'article contient une analyse plus détaillée des principes et des exemples détaillés tirés de la pratique qui ne correspondaient pas au rapport. Je vous recommande de le lire si vous souhaitez approfondir le sujet et apprendre comment nous écrivons des composants réutilisables. Si vous souhaitez vous familiariser avec le monde des composants réutilisables en termes généraux, alors, à mon avis, l'enregistrement du rapport vous convient mieux.






Tout le monde sait que la duplication de code est mauvaise car on en parle souvent : dans un livre sur votre premier langage de programmation, dans des cours de programmation, dans des livres sur l'écriture de code de qualité comme Perfect Code et Clean Code.



Voyons pourquoi il est si difficile d'éviter la duplication dans le frontend et comment écrire correctement des composants réutilisables. Et les principes de SOLID nous aideront.



Pourquoi est-il difficile d'arrêter de dupliquer du code ?



Il semblerait que le principe semble simple. Et en même temps, il est facile de vérifier s'il est respecté : s'il n'y a pas de duplication dans la base de code, alors tout va bien. Pourquoi est-ce si difficile en pratique ?



Analysons un exemple avec une bibliothèque de composants Ya. Tutoriel. Il était une fois, le projet était un monolithe. Plus tard, pour plus de commodité, les développeurs ont décidé de déplacer les composants réutilisables dans une bibliothèque distincte. L'un des premiers à y arriver était un composant de bouton. Le composant a évolué, au fil du temps, de nouvelles « compétences » et des paramètres pour le bouton sont apparus, le nombre de personnalisations visuelles a augmenté. Après un certain temps, le composant est devenu si sophistiqué qu'il est devenu difficile de l'utiliser pour de nouvelles tâches et de l'étendre davantage.



Et donc, à l'itération suivante, une copie du composant est apparue - Button2. Cela s'est passé il y a très longtemps, personne ne se souvient des raisons exactes de l'apparition. Cependant, le composant a été créé.







Il semblerait que tout va bien - qu'il y ait deux boutons. Après tout, ce n'est qu'un bouton. Cependant, en réalité, avoir deux boutons dans un projet avait des conséquences à long terme très désagréables.



A chaque fois, lorsqu'il était nécessaire de mettre à jour les styles, il n'était pas clair dans quel composant le faire. J'ai dû vérifier où quel composant est utilisé afin de ne pas casser accidentellement les styles à d'autres endroits. Lorsqu'une nouvelle version de l'affichage des boutons est apparue, nous avons décidé quels composants étendre. Chaque fois que nous voyions une nouvelle fonctionnalité, nous réfléchissions aux boutons à utiliser. Et parfois, à un endroit, nous avions besoin de plusieurs boutons différents, puis nous importions deux composants de bouton dans un composant de projet à la fois.



Malgré le fait qu'à long terme l'existence des deux composants du bouton s'est avérée douloureuse, nous n'avons pas immédiatement compris la gravité du problème et avons réussi à faire quelque chose de similaire avec les icônes. Nous avons créé un composant, et quand nous avons réalisé que ce n'était pas très pratique pour nous, nous avons fait Icon2, et quand il s'est avéré inadapté à de nouvelles tâches, nous avons écrit Icon3.



Presque tout l'ensemble des effets négatifs de la duplication de boutons a été répété dans les composants d'icônes. C'était un peu plus facile car les icônes sont moins utilisées dans le projet. Bien que, pour être honnête, tout dépend de la fonctionnalité. De plus, tant pour le bouton que pour l'icône, l'ancien composant n'était pas supprimé lors de la création d'un nouveau, car la suppression nécessitait beaucoup de refactorisation avec l'apparition possible de bugs tout au long du projet. Alors, qu'est-ce que les étuis à boutons et icônes ont en commun ? Le même schéma pour l'apparition de doublons dans le projet. Il nous était difficile de réutiliser le composant actuel, de l'adapter aux nouvelles conditions, nous en avons donc créé un nouveau.



En créant un duplicata d'un composant, nous compliquons notre vie future. Nous voulions assembler une interface à partir de blocs prêts à l'emploi, à la manière d'un constructeur. Pour ce faire facilement, vous avez besoin de composants de qualité que vous pouvez simplement prendre et utiliser. La racine du problème est que le composant que nous prévoyons de réutiliser a été écrit de manière incorrecte. Il était difficile de l'étendre et de l'appliquer ailleurs.



Un composant réutilisable doit être suffisamment polyvalent et simple à la fois. Travailler avec lui ne devrait pas causer de douleur et ressembler à tirer sur un moineau avec un canon. D'un autre côté, le composant doit être suffisamment personnalisable pour qu'avec un petit changement dans le script, il ne devienne pas clair qu'il est plus facile d'écrire "Component2".



SOLIDE vers des composants réutilisables



Pour écrire des composants de qualité, nous avons besoin d'un ensemble de règles derrière l'acronyme SOLID. Ces règles expliquent comment combiner des fonctions et des structures de données dans des classes, et comment les classes doivent être combinées entre elles.



Pourquoi exactement SOLID et pas un autre ensemble de principes ? Les règles SOLID vous indiquent comment concevoir correctement votre application. Pour que vous puissiez développer le projet en toute sécurité, ajouter de nouvelles fonctions, modifier celles existantes et en même temps ne pas tout casser. Lorsque j'ai essayé de décrire ce que, à mon avis, devrait être un bon composant, je me suis rendu compte que mes critères sont proches des principes de SOLID.



  • S est le principe de la responsabilité exclusive.
  • O - le principe d'ouverture / proximité.
  • L est le principe de substitution de Liskov.
  • I - le principe de séparation des interfaces.
  • D - Principe d'inversion de dépendance.


Certains de ces principes fonctionnent bien pour décrire les composants. D'autres semblent plus tirés par les cheveux dans un contexte frontal. Mais tous ensemble, ils décrivent bien ma vision d'un composant de qualité.



Nous suivrons les principes dans le désordre, mais du simple au complexe. Tout d'abord, examinons les éléments de base qui peuvent être utiles dans un grand nombre de situations, puis les éléments plus puissants et spécifiques.



L'article fournit des exemples de code dans React + TypeScript. J'ai choisi React comme framework avec lequel je travaille le plus. A sa place peut être n'importe quel autre cadre que vous aimez ou convenez. Au lieu de TS, il peut y avoir du JS pur, mais TypeScript vous permet de décrire explicitement les contrats dans le code, ce qui simplifie le développement et l'utilisation de composants complexes.



De base



Le principe ouverture/fermeture



Les entités logicielles doivent être ouvertes pour extension et fermées pour modification. En d'autres termes, nous devrions être en mesure d'étendre les fonctionnalités avec un nouveau code sans modifier l'existant. Pourquoi c'est important? Si à chaque fois que vous devez éditer un tas de modules existants pour ajouter de nouvelles fonctionnalités, l'ensemble du projet deviendra instable. Il y aura beaucoup d'endroits qui peuvent casser en raison du fait qu'ils sont constamment modifiés.



Considérons l'application du principe sur l'exemple d'un bouton. Nous avons créé un composant bouton et il a des styles. Jusqu'à présent, tout a bien fonctionné. Mais ensuite, une nouvelle tâche arrive, et il s'avère qu'à un endroit spécifique pour ce bouton, différents styles doivent être appliqués.





Le bouton est écrit de telle manière qu'il ne peut pas être modifié sans modifier le code



Pour appliquer différents styles dans la version actuelle, vous devrez modifier le composant de bouton. Le problème est que le composant n'est pas personnalisable. Nous n'envisagerons pas l'option d'écrire des styles globaux, car elle n'est pas fiable. Tout peut casser avec n'importe quelle modification. Les conséquences sont faciles à imaginer si vous mettez quelque chose de plus complexe à la place du bouton, par exemple, un composant sélecteur de date.



Selon le principe d'ouverture/fermeture, nous devons écrire le code de sorte que lors de l'ajout d'un nouveau style, nous n'ayons pas à réécrire le code du bouton. Tout s'arrangera si une partie des styles du composant peut être jetée à l'extérieur. Pour ce faire, nous allons créer un prop dans lequel nous passerons la classe requise pour décrire les nouveaux styles du composant.



//    ,    
import cx from 'classnames';

//    — mix
const Button = ({ children, mix }) => {
  return (
    <button
      className={cx("my-button", mix)}
    >
      {children}
    </button>
}

      
      





C'est fait, vous n'avez plus besoin de modifier son code pour personnaliser un composant.







Cette méthode assez populaire permet de personnaliser l'apparence d'un composant. C'est ce qu'on appelle un mélange car la classe supplémentaire est mélangée avec les propres classes du composant. Notez que passer une classe n'est pas le seul moyen de styliser un composant de l'extérieur. Vous pouvez passer un objet avec des propriétés CSS à un composant. Vous pouvez utiliser des solutions CSS-in-JS, l'essence ne changera pas. Les mix sont utilisés par de nombreuses bibliothèques de composants, par exemple : MaterialUI, Vuetify, PrimeNG et autres.



Quelle conclusion peut-on tirer des mélanges ? Ils sont faciles à mettre en œuvre, polyvalents et vous permettent de personnaliser de manière flexible l'apparence de vos composants avec un minimum d'effort.



Mais cette approche a aussi ses inconvénients. Il laisse beaucoup de liberté, ce qui peut conduire à des problèmes avec la spécificité des sélecteurs. Il casse également l'encapsulation. Afin de générer le sélecteur CSS correct, vous devez connaître la structure interne du composant. Cela signifie qu'un tel code peut casser lors de la refactorisation d'un composant.



Variabilité des composants



Un composant a des parties qui sont son noyau. Si nous les changeons, nous obtenons un composant différent. Pour un bouton, c'est un ensemble d'états et de comportements. Les utilisateurs distinguent un bouton d'une case à cocher, par exemple, grâce à son effet de survol et de clic. Il existe une logique générale de travail : lorsque l'utilisateur clique, le gestionnaire d'événements est déclenché. C'est le cœur du composant, ce qui fait d'un bouton un bouton. Oui, il y a des exceptions, mais c'est ainsi que cela fonctionne dans la plupart des cas d'utilisation.



Il y a aussi des pièces dans le composant qui peuvent changer en fonction du lieu d'utilisation. Les styles appartiennent à ce groupe. Peut-être avons-nous besoin d'un bouton d'une taille ou d'une couleur différente. Avec un trait et un congé différents, ou avec un effet de survol différent. Tous les styles sont une partie modifiable du composant. Nous ne voulons pas réécrire ou créer un nouveau composant chaque fois que le bouton est différent.



Ce qui change fréquemment doit être modifié sans changer le code. Sinon, nous nous retrouverons dans une situation où il est plus facile de créer un nouveau composant que de personnaliser et d'ajouter un ancien, qui s'est avéré insuffisamment flexible.



Thématique



Revenons à la personnalisation du visuel du composant en utilisant l'exemple d'un bouton. La méthode suivante consiste à appliquer des thèmes. Par thématisation, j'entends la capacité d'un composant à apparaître dans plusieurs modes, différemment à différents endroits. Cette interprétation est plus large que la thématisation dans le contexte des thèmes clairs et sombres.



L'utilisation de thèmes n'exclut pas la méthode précédente avec des mélanges, mais la complète. Nous disons explicitement qu'un composant a plusieurs méthodes d'affichage et, lorsqu'il est utilisé, vous oblige à spécifier celle souhaitée.



import cx from 'classnames';
import b from 'b_';

const Button = ({ children, mix, theme }) => (
  <button
    className={cx(
     b("my-button", { theme }), mix)}
  >
    {children}
  </button>
)

      
      





La thématisation vous permet d'éviter le zoo de styles, lorsque, par exemple, vous avez 20 boutons dans votre projet et qu'ils ont tous un aspect un peu différent du fait que les styles de chaque bouton sont définis à l'endroit de l'application. L'approche peut être appliquée à tous les nouveaux composants sans crainte de sur-ingénierie. Si vous comprenez qu'un composant peut avoir une apparence différente, il est préférable de définir explicitement le thème dès le début. Cela simplifiera le développement ultérieur des composants.



Mais il y a aussi un inconvénient - la méthode n'est adaptée qu'à la personnalisation du visuel et ne permet pas d'influencer le comportement du composant.



Imbrication de composants



Je n'ai pas listé toutes les manières d'éviter de changer le code composant lors de l'ajout de nouvelles fonctions. D'autres seront démontrés en examinant le reste des principes. Ici, je voudrais mentionner les composants enfants et les emplacements.



La page Web est une hiérarchie arborescente de composants. Chaque composant décide pour lui-même quoi et comment rendre. Mais ce n'est pas toujours le cas. Par exemple, un bouton vous permet de spécifier quel contenu sera rendu en interne. Dans React, l'outil principal est l'accessoire enfants et les accessoires de rendu. Vue a un concept de machines à sous plus puissant. Il n'y a aucun problème lors de l'écriture de composants simples à l'aide de ces capacités. Mais il est important de ne pas oublier que même dans les composants complexes, vous pouvez utiliser le lancement de certains des éléments que le composant doit afficher d'en haut.



Avancée



Les principes décrits ci-dessous conviennent aux grands projets. Les techniques correspondantes donnent plus de flexibilité, mais augmentent la complexité de la conception et du développement.



Principe de responsabilité unique



Le principe de responsabilité unique signifie qu'un module doit avoir une et une seule raison de changer.



Pourquoi c'est important? Les conséquences de la violation du principe comprennent :

  • Risque d'en casser une autre lors de l'édition d'une partie du système.
  • Mauvaises abstractions. Le résultat est des composants qui peuvent exécuter plusieurs fonctions, ce qui rend difficile de comprendre exactement ce que le composant doit faire et ce qu'il ne doit pas faire.
  • Travail peu pratique avec des composants. Il est très difficile d'apporter des améliorations ou de corriger des bogues dans un composant qui fait tout à la fois.


Revenons à l'exemple de la thématique et voyons si le principe de responsabilité unique est respecté. Déjà dans sa forme actuelle, la thématisation fait face à ses tâches, mais cela ne signifie pas que la solution n'a pas de problèmes et qu'elle ne peut pas être améliorée.





Un module est édité par différentes personnes pour différentes raisons.



Disons que nous mettons tous les styles dans un seul fichier css. Il peut être modifié par différentes personnes pour différentes raisons. Il s'avère que le principe de la responsabilité exclusive a été violé. Quelqu'un peut refactoriser les styles et un autre développeur ajustera la nouvelle fonctionnalité. Ainsi, vous pouvez facilement casser quelque chose.



Jetons un coup d'œil à ce à quoi pourrait ressembler un thème compatible SRP. L'image parfaite : nous avons un bouton et, séparément, un ensemble de thèmes pour cela. Nous pouvons appliquer un thème à un bouton et obtenir un bouton thématique. En bonus, j'aimerais pouvoir assembler un bouton avec plusieurs thèmes disponibles, par exemple, pour être placé dans une bibliothèque de composants.





Peinture souhaitée. Un thème est une entité distincte et peut être appliqué à un bouton. Un



thème enveloppe un bouton. C'est l'approche utilisée dans Lego, notre bibliothèque de composants internes. Nous utilisons HOC (High Order Components) pour envelopper le composant de base et lui ajouter de nouvelles fonctionnalités. Par exemple, la possibilité d'afficher avec un thème.



HOC est une fonction qui prend un composant et renvoie un autre composant. Un HOC avec un thème peut lancer un objet avec des styles à l'intérieur du bouton. Vous trouverez ci-dessous une option plutôt pédagogique, dans la vraie vie, vous pouvez utiliser des solutions plus élégantes, par exemple, lancer une classe dans le composant, dont les styles sont importés dans le HOC, ou utiliser des solutions CSS-in-JS.



Un exemple de HOC simple pour thématiser un bouton :



const withTheme1 = (Button) =>
(props) => {
    return (
        <Button
            {...props}
            styles={theme1Styles}
        />
    )
}

const Theme1Button = withTheme1(Button);
      
      





Le HOC ne peut appliquer des styles que si un thème spécifique est spécifié, sinon il ne fait rien. Nous pouvons donc assembler un bouton avec un ensemble de thèmes et activer celui dont nous avons besoin en spécifiant le thème prop.



Utilisation de plusieurs HOC pour collecter un bouton avec les thèmes souhaités :



import "./styles.css";
 
//   .     
const ButtonBase = ({ style, children }) => {
 console.log("styl123e", style);
 return <button style={style}>{children}</button>;
};
 
const withTheme1 = (Button) => (props) => {
 // HOC  ,     "theme1"
 if (props.theme === "theme1") {
   return <Button {...props} style={{ color: "red" }} />;
 }
 
 return <Button {...props} />;
};
 
const withTheme2 = (Button) => (props) => {
 // HOC  ,     "theme2"
 if (props.theme === "theme2") {
   return <Button {...props} style={{ color: "green" }} />;
 }
 
 return <Button {...props} />;
};
 
// -      HOC
const compose = (...hocs) => (BaseComponent) =>
 hocs.reduce((Component, nextHOC) => nextHOC(Component), BaseComponent);
 
//  ,    
const Button = compose(withTheme1, withTheme2)(ButtonBase);
 
export default function App() {
 return (
   <div className="App">
     <Button theme="theme1">"Red"</Button>
     <Button theme="theme2">"Green"</Button>
   </div>
 );
}

      
      





Et ici, nous arrivons à la conclusion que nous devons diviser les domaines de responsabilité. Même s'il semble que vous ayez un composant, pensez - est-ce vraiment le cas ? Peut-être devrait-il être divisé en plusieurs couches, chacune étant responsable d'une fonction spécifique. Dans presque tous les cas, la couche visuelle peut être découplée de la logique du composant.



Séparer un thème en une entité distincte donne des avantages à la convivialité du composant : vous pouvez placer un bouton dans une bibliothèque avec un ensemble de thèmes de base et permettre aux utilisateurs d'écrire les leurs si nécessaire ; les sujets peuvent être facilement tâtés entre les projets. Cela vous permet de préserver la cohérence de l'interface et de ne pas surcharger la bibliothèque d'origine.



Il existe différentes options pour mettre en œuvre la division en couches. L'exemple ci-dessus était avec HOC, mais la composition est également possible. Cependant, je pense que dans le cas de la thématisation, les HOC sont plus appropriés, car le thème n'est pas un composant autonome.



Ce n'est pas seulement le visuel qui peut être placé dans une couche séparée. Mais je n'envisage pas d'étudier en détail le transfert de la logique métier vers le HOC, car la question est très holistique. Mon opinion est que vous pouvez le faire si vous comprenez ce que vous faites et pourquoi vous en avez besoin.



Composants composites



Passons à des composants plus complexes. Prenons Select comme exemple et voyons à quoi sert le principe de responsabilité unique. Select peut être considéré comme une composition de composants plus petits.







  • Conteneur - communication entre d'autres composants.
  • Champ - le texte pour la sélection habituelle et l'entrée pour le composant CobmoBox, où l'utilisateur entre quelque chose.
  • Icône - une icône traditionnelle dans le domaine pour sélectionner.
  • Le menu est un composant qui affiche une liste d'éléments à sélectionner.
  • L'élément est un élément distinct dans le menu.


Pour respecter le principe de responsabilité unique, vous devez regrouper toutes les entités dans des composants distincts, en laissant à chacun une seule raison de modifier. Lorsque nous couperons le fichier, la question se posera : comment maintenant personnaliser l'ensemble de composants résultant ? Par exemple, si vous devez définir un thème sombre pour le champ, agrandissez l'icône et modifiez la couleur du menu. Il y a deux façons d'y parvenir.



Dérogations



La première façon est simple. Déplacez tous les paramètres des composants imbriqués vers les accessoires de l'original. Cependant, si vous appliquez la solution "de front", il s'avère que le select a un grand nombre d'accessoires, qui sont difficiles à comprendre. Il est nécessaire de les organiser d'une manière ou d'une autre commodément. C'est là qu'intervient la dérogation. Il s'agit d'une configuration qui est transmise à un composant et vous permet de personnaliser chacun de ses éléments.



<Select
  ...
  overrides={{
    Field: {
      props: {theme: 'dark'}
    },
    Icon: {
      props: {size: 'big'},
    },
    Menu: {
      style: {backgroundColor: '#CCCCCC'}
    },
  }}
/>

      
      





J'ai donné un exemple simple où l'on remplace les props. Mais l'override peut être considéré comme une configuration globale - il configure tout ce que les composants prennent en charge. Vous pouvez voir comment cela fonctionne en pratique dans la bibliothèque BaseWeb .



Dans l'ensemble, en utilisant la substitution, vous pouvez personnaliser de manière flexible les composants composites, et cette approche s'adapte également bien. Inconvénients : les configurations pour les composants complexes s'avèrent très volumineuses et la puissance de remplacement a un inconvénient. Nous obtenons un contrôle total sur les composants internes, ce qui nous permet de faire des choses étranges et d'exposer des paramètres invalides. De plus, si vous n'utilisez pas de bibliothèques, mais que vous souhaitez implémenter l'approche vous-même, vous devrez apprendre aux composants à comprendre la configuration ou à écrire des wrappers qui la liront et configureront correctement les composants.



Principe d'inversion de dépendance



Pour comprendre l'alternative aux configurations de remplacement, tournons-nous vers la lettre D dans SOLID. C'est le principe d'inversion de dépendance. Il soutient que le code qui implémente la politique de haut niveau ne devrait pas dépendre du code qui implémente les détails de bas niveau.



Revenons à notre sélection. Le conteneur est responsable de la communication entre les autres parties du composant. En fait, c'est la racine qui contrôle le rendu du reste des blocs. Pour ce faire, il doit les importer.



Voici à quoi ressemblera la racine de tout composant complexe, si vous n'utilisez pas l'inversion de dépendance :



import InputField from './InputField';
import Icon from './Icon';
import Menu from './Menu';
import Option from './Option';
      
      





Analysons les dépendances entre les composants pour comprendre ce qui peut mal se passer. Maintenant, la sélection de niveau supérieur dépend du menu de niveau inférieur, car elle l'importera en elle-même. Le principe d'inversion de dépendance est rompu. Cela crée des problèmes.

  • Tout d'abord, si vous modifiez le menu, vous devrez modifier Select.
  • Deuxièmement, si nous voulons utiliser un composant de menu différent, nous devons également éditer le composant select.




Il n'est pas clair quoi faire lorsque vous avez besoin de Sélectionner avec un menu différent.



Vous devez étendre la dépendance. Faites en sorte que le composant de menu dépende de la sélection. L'inversion de dépendance se fait par injection de dépendance - Select doit accepter un composant de menu comme l'un des paramètres, props. C'est là que la dactylographie est utile. Nous indiquerons quel composant le Select attend.



//     Select      
const Select = ({
  Menu: React.ComponentType<IMenu>
}) => {
  return (
    ...
    <Menu>
      ...
    </Menu>
    ...
  )
...
}

      
      





C'est ainsi que nous déclarons que le select a besoin d'un composant de menu dont les props satisfont à une certaine interface. Ensuite, les flèches pointent dans la direction opposée, comme le dicte le principe DI.





La flèche est agrandie, c'est ainsi que fonctionne l'inversion de dépendance.



Nous avons résolu le problème de dépendance, mais un peu de sucre syntaxique et des outils d'aide sont les bienvenus ici.



À chaque fois, jeter toutes les dépendances dans un composant à l'emplacement de rendu est fastidieux, mais la bibliothèque bem-react dispose d'un registre de dépendances et d'un processus de composition. Avec leur aide, vous pouvez empaqueter les dépendances et les paramètres une fois, puis utiliser simplement le composant prêt à l'emploi.



import { compose } from '@bem-react/core'
import { withRegistry, Registry } from '@bem-react/di'

const selectRegistry = new Registry({ id: cnSelect() })

...

selectRegistry.fill({
    'Trigger': Button,
    'Popup': Popup,
    'Menu': Menu,
    'Icon': Icon,
})

const Select = compose(
    ...
    withRegistry(selectRegistry),
)(SelectDesktop)
      
      





L'exemple ci-dessus montre une partie de l'assemblage de composants à l'aide de l'exemple bem-react. L'exemple de code complet et le bac à sable se trouvent dans le livre d'histoires de l' interface utilisateur de Yandex .



Qu'obtenons-nous de l'utilisation de l'inversion de dépendance ?



  • Contrôle total - la liberté de personnaliser tous les composants d'un composant.
  • Encapsulation flexible - la possibilité de rendre les composants très flexibles et entièrement personnalisables. Si nécessaire, le développeur écrasera tous les blocs qui composent le composant et obtiendra ce qu'il veut. Dans ce cas, il existe toujours une option pour créer des composants déjà configurés et prêts à l'emploi.
  • Évolutivité - Cette méthode fonctionne bien pour les bibliothèques de toute taille.


Chez Yandex.Tutorial, nous écrivons nos propres composants à l'aide de DI. La bibliothèque interne de composants Lego adopte également cette approche. Mais il a un inconvénient important - un développement beaucoup plus complexe.



Difficultés à développer des composants réutilisables



Quelle est la difficulté de développer des composants réutilisables ?



Tout d'abord, une conception longue et soignée. Vous devez comprendre de quelles pièces sont constitués les composants et quelles pièces peuvent changer. Si nous rendons toutes les parties mutables, nous nous retrouvons avec un nombre énorme d'abstractions difficiles à comprendre. S'il y a trop peu de pièces modifiables, le composant ne sera pas assez flexible. Il devra être amélioré pour éviter de futurs problèmes de réutilisation.



Deuxièmement, les exigences élevées pour les composants. Vous comprenez de quelles pièces les composants seront constitués. Maintenant, vous devez les écrire pour qu'ils ne sachent rien l'un de l'autre, mais puissent être utilisés ensemble. C'est plus difficile que de développer sans tenir compte de la réutilisation.



Troisièmement, une structure complexe en conséquence des points précédents. Si vous avez besoin d'une personnalisation sérieuse, vous devrez reconstruire toutes les dépendances du composant. Pour ce faire, vous devez comprendre en profondeur de quelles parties il se compose. Une bonne documentation est essentielle dans le processus.



Le didacticiel dispose d'une bibliothèque de composants internes où se trouvent les mécanismes éducatifs - une partie de l'interface avec laquelle les enfants interagissent tout en résolvant des problèmes. Et puis il y a une bibliothèque partagée de services éducatifs. Nous y mettons les composants que nous souhaitons réutiliser entre différents services.



Le transfert d'un mécanicien prend plusieurs semaines, à condition que nous ayons déjà un composant fonctionnel et que nous n'ajoutions pas de nouvelle fonctionnalité. L'essentiel de ce travail consiste à scier le composant en morceaux indépendants et à permettre leur partage.



Principe de substitution de Liskov



Les principes précédents concernaient ce qu'il faut faire, et les deux derniers concerneront ce qu'il ne faut pas casser.



Commençons par le principe de substitution de Barbara Liskov. Il dit que les objets d'un programme devraient être remplaçables par des instances de leurs sous-types sans interrompre l'exécution correcte du programme.



Nous n'écrivons généralement pas les composants sous forme de classes et nous n'utilisons pas l'héritage. Tous les composants sont interchangeables hors de la boîte. C'est le fondement du frontend moderne. Une frappe forte vous aide à éviter les erreurs et à maintenir la compatibilité.



Comment la remplaçabilité prête à l'emploi peut-elle s'effondrer ? Le composant a une API. Par API, j'entends un ensemble d'accessoires pour un composant et des mécanismes intégrés au framework, tels qu'un mécanisme de gestionnaire d'événements. Une frappe et un linting forts dans l'IDE peuvent mettre en évidence une incompatibilité dans l'API, mais le composant peut interagir avec le monde extérieur et contourner l'API :



  • lire et écrire quelque chose dans le magasin global,
  • interagir avec la fenêtre,
  • interagir avec les cookies,
  • lecture/écriture stockage local,
  • faire des requêtes au réseau.






Tout cela est dangereux, car le composant dépend de l'environnement et peut se casser si vous le déplacez vers un autre endroit ou vers un autre projet.



Pour respecter le principe de substitution de Liskov, il vous faut :



  • utiliser les capacités de saisie,
  • éviter les interactions en contournant l'API du composant,
  • éviter les effets secondaires.


Comment éviter les interactions non-API ? Mettez tout ce dont le composant dépend dans l'API et écrivez un wrapper qui transmettra les données du monde extérieur aux accessoires. Par exemple, comme ceci :

const Component = () => {
   /*
       ,     ,       .
      ,          .
          ,   ,   .
   */
   const {userName} = useStore();
 
   //     ,       ( ,       ).
   const userToken = getFromCookie();
 
   //  —   window      .
   const {taskList} = window.ssrData;
 
   const handleTaskUpdate = () => {
       //    API .      .
       fetch(...)
   }
 
   return <div>{'...'}</div>;
  };
 
/*
          .
      ,         .
*/
const Component2 = ({
   userName, userToken, onTaskUpdate
}) => {
   return <div>{'...'}</div>;
};

      
      





Principe de séparation des interfaces



De nombreuses interfaces à usage spécial valent mieux qu'une seule interface à usage général. Je n'ai pas pu transférer le principe aux composants front-end aussi clairement. Je comprends donc cela comme un besoin de garder un œil sur l'API.



Il est nécessaire de transférer le moins d'entités possible vers le composant et de ne pas transférer de données qui ne sont pas utilisées par celui-ci. Un grand nombre d'accessoires dans un composant est une raison de se méfier. Très probablement, il viole les principes SOLID.



Où et comment réutiliser ?



Nous avons discuté des principes pour vous aider à écrire des composants de qualité. Voyons maintenant où et comment nous allons les réutiliser. Cela vous aidera à comprendre les autres problèmes que vous pourriez rencontrer.



Le contexte peut être différent : vous devez utiliser un composant ailleurs sur la même page, ou, par exemple, vous souhaitez le réutiliser dans d'autres projets d'entreprise - ce sont des choses complètement différentes. Je souligne plusieurs options :



Aucune réutilisation n'est encore requise.Vous avez écrit un composant, vous pensez qu'il est spécifique et n'envisagez pas de l'utiliser ailleurs. Il n'est pas nécessaire de faire des efforts supplémentaires. Et vous pouvez effectuer quelques étapes simples qui vous seront utiles si vous souhaitez toujours y revenir. Ainsi, par exemple, vous pouvez vérifier que le composant n'est pas trop lié à l'environnement et que les dépendances sont encapsulées. Vous pouvez aussi faire une réserve pour la personnalisation pour l'avenir : ajout de thèmes ou possibilité de changer l'apparence d'un composant de l'extérieur (comme dans l'exemple avec un bouton) - cela ne prend pas beaucoup de temps.



Réutiliser dans le même projet.Vous avez écrit un composant et vous êtes sûr de vouloir le réutiliser ailleurs dans votre projet actuel. Tout ce qui est écrit ci-dessus est pertinent ici. Seulement maintenant, il est impératif de supprimer toutes les dépendances dans les wrappers externes et il est hautement souhaitable de pouvoir personnaliser de l'extérieur (thèmes ou mixages). Si un composant contient beaucoup de logique, vous devez vous demander s'il est nécessaire partout ou s'il doit être modifié à certains endroits. Pour la deuxième option, envisagez la possibilité de personnalisation. Il est également important ici de réfléchir à la structure du composant et de le décomposer en plusieurs parties si nécessaire.



Réutiliser sur une pile similaire.Vous comprenez que le composant sera utile dans un projet voisin qui a la même pile que vous. C'est là que tout ce qui précède devient obligatoire. De plus, je vous conseille de surveiller de près les dépendances et les technologies. Le projet voisin utilise-t-il exactement les mêmes versions des bibliothèques que vous ? SASS et TypeScript utilisent-ils la même version ?



Je voudrais également souligner la réutilisation dans un autre environnement d'exécution , par exemple, dans SSR. Décidez si votre composant peut et doit être capable d'effectuer un rendu sur SSR. Si c'est le cas, assurez-vous qu'il s'affiche comme prévu au préalable. N'oubliez pas qu'il existe d'autres runtimes comme deno ou GraalVM. Tenez compte de leurs caractéristiques si vous les utilisez.



Bibliothèques de composants



Si des composants doivent être réutilisés entre plusieurs référentiels et/ou projets, ils doivent être déplacés vers la bibliothèque.



Empiler



Plus les technologies sont utilisées dans les projets, plus il sera difficile de résoudre les problèmes de compatibilité. Il est préférable de réduire le zoo et de minimiser le nombre de technologies utilisées : frameworks, langages, versions de grosses bibliothèques. Si vous comprenez que vous avez vraiment besoin de beaucoup de technologie, vous devrez apprendre à vivre avec. Par exemple, vous pouvez utiliser des wrappers sur des composants Web, tout collecter en JS pur ou utiliser des adaptateurs pour les composants.



La taille



Si l'utilisation d'un simple composant de votre bibliothèque ajoute quelques mégaoctets au bundle, ce n'est pas ok. De tels composants ne veulent pas être réutilisés, car la perspective d'écrire votre propre version allégée à partir de zéro semble justifiée. Vous pouvez résoudre le problème à l'aide d'outils de contrôle de taille, par exemple, la limite de taille.



N'oubliez pas la modularité - un développeur qui souhaite utiliser votre composant doit pouvoir le prendre uniquement et ne pas faire glisser tout le code de la bibliothèque dans le projet.



Il est important que la bibliothèque modulaire ne soit pas compilée dans un seul fichier. Vous devez également garder une trace de la version JS vers laquelle la bibliothèque va. Si vous construisez la bibliothèque dans ES.NEXT et les projets dans ES5, il y aura des problèmes. Vous devez également configurer correctement l'assembly pour les anciennes versions des navigateurs et vous assurer que tous les utilisateurs de la bibliothèque savent dans quoi il va. Si c'est trop compliqué, il existe une alternative - mettre en place vos propres règles de construction de bibliothèque dans chaque projet.



Mettre à jour



Réfléchissez à l'avance à la façon dont vous allez mettre à jour la bibliothèque. C'est bien si vous connaissez tous les clients et leurs scripts personnalisés. Cela vous aidera à mieux réfléchir aux migrations et aux changements de rupture. Par exemple, une équipe utilisant votre bibliothèque trouvera extrêmement frustrant d'apprendre une mise à jour majeure avec des changements de rupture avant la publication.



En déplaçant des composants dans une bibliothèque que quelqu'un d'autre utilise, vous perdez la facilité de la refactorisation. Pour éviter que le fardeau de la refactorisation ne devienne écrasant, je vous conseille de ne pas faire glisser de nouveaux composants dans les bibliothèques. Ils sont susceptibles de changer, ce qui signifie que vous devrez passer beaucoup de temps à mettre à jour et à maintenir la compatibilité.



Personnalisation et conception



La conception n'affecte pas la réutilisation, mais constitue une partie importante de la personnalisation. Dans notre Tutoriel, les composants ne vivent pas seuls, leur apparence est conçue par des designers. Les concepteurs ont un système de conception. Si un composant semble différent dans le système et dans le référentiel, les problèmes ne peuvent être évités. Les concepteurs et les développeurs n'ont pas les mêmes idées sur l'apparence de l'interface, ce qui peut conduire à de mauvaises décisions.



Vitrine de composants



La vitrine des composants contribuera à simplifier l'interaction avec les concepteurs. L'une des solutions de vitrine les plus populaires est Storybook . En utilisant cet outil ou un autre outil approprié, vous pouvez montrer les composants du projet à toute personne en dehors du développement.



Ajoutez de l'interactivité à la vitrine - les concepteurs doivent pouvoir interagir avec les composants et voir comment ils s'affichent et fonctionnent avec différents paramètres.



N'oubliez pas de configurer la vitrine pour qu'elle se mette à jour automatiquement lorsque vous mettez à jour des composants. Pour ce faire, vous devez déplacer le processus vers CI. Désormais, les concepteurs peuvent toujours voir quels composants prêts à l'emploi se trouvent dans le projet et les utiliser.







Système de conception



Pour un développeur, un système de conception est un ensemble de règles qui régissent l'apparence des composants d'un projet. Pour empêcher le zoo de composants de se développer, vous pouvez limiter la personnalisation à ses limites.



Un autre point important est que le système de conception et le type de composants du projet diffèrent parfois les uns des autres. Par exemple, lorsqu'il y a une grande refonte et que tout ne peut pas être mis à jour dans le code, ou que vous devez ajuster un composant, mais vous n'avez pas le temps d'apporter des modifications au système de conception. Dans ces cas, il est à la fois dans votre intérêt et dans l'intérêt des concepteurs de synchroniser le système de conception et le projet dès que l'occasion se présente.



Dernier conseil universel et évident : communiquer et négocier. Il n'est pas nécessaire de percevoir les concepteurs comme ceux qui se tiennent à l'écart du développement et ne font que créer et modifier des mises en page. Travailler en étroite collaboration avec eux vous aidera à concevoir et à mettre en œuvre une interface de qualité. En fin de compte, cela profitera à la cause commune et ravira les utilisateurs du produit.



conclusions



Le code dupliqué entraîne des difficultés de développement et une diminution de la qualité du frontend. Pour éviter les conséquences, vous devez surveiller la qualité des composants et les principes de SOLID aident à écrire des composants de qualité.



Il est beaucoup plus difficile d'écrire un bon composant avec une réserve pour l'avenir que celui qui résout rapidement le problème ici et maintenant. En même temps, de bonnes « briques » ne sont qu'une partie de la solution. Si vous apportez des composants à la bibliothèque, vous devez faciliter leur utilisation et ils doivent également être synchronisés avec le système de conception.



Comme vous pouvez le voir, la tâche n'est pas facile. Il est difficile et long de développer des composants réutilisables de haute qualité. Est-ce que ça vaut le coup? Je pense que chacun répondra à cette question par lui-même. Pour les petits projets, les frais généraux peuvent être trop élevés. Pour les projets où le développement à long terme n'est pas prévu, investir des efforts dans la réutilisation du code est également une décision controversée. Cependant, après avoir dit « nous n'en avons pas besoin maintenant », il est facile d'ignorer comment vous vous retrouvez dans une situation où le manque de composants réutilisables entraînera de nombreux problèmes qui n'auraient peut-être pas eu lieu. Alors ne répétez pas nos erreurs et ne vous répétez pas !



Regardez le reportage



All Articles