Appliquer les principes SOLID pour réagir au développement d'applications

Nous avons récemment publié des documents sur la méthodologie SOLID. Aujourd'hui, nous attirons votre attention sur la traduction d'un article qui traite de l'application des principes SOLID dans le développement d'applications à l'aide de la populaire bibliothèque React.

image

L'auteur de l'article dit qu'ici, par souci de concision, il ne montre pas la mise en œuvre complète de certains composants.

Principe de responsabilité unique (S)


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

Imaginez que nous développons une application qui affiche une liste d'utilisateurs dans un tableau. Voici le code du composant App :

 class App extends Component {   state = {       users: [{name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       this.fetchUsers();   }   async fetchUsers() {       const response = await fetch('http://totallyhardcodedurl.com/users');       const users = await response.json();       this.setState({users});   }   render() {       return (           <div className="App">               <header className="App-header">                 //                     </header>               <table>                   <thead>                       <tr>                           <th>First name</th>                           <th>Last name</th>                           <th>Age</th>                       </tr>                   </thead>                   <tbody>                       {this.state.users.map((user, index) => (                           <tr key={index}>                               <td><input value={user.name} onChange={/* update name in the state */}/></td>                               <td><input value={user.surname} onChange={/* update surname in the state*/}/></td>                               <td><input value={user.age} onChange={/* update age in the state */}/></td>                           </tr>                       ))}                   </tbody>               </table>               <button onClick={() => this.saveUsersOnTheBackend()}>Save</button>           </div>       );   }   saveUsersOnTheBackend(row) {       fetch('http://totallyhardcodedurl.com/users', {           method: "POST",           body: JSON.stringify(this.state.users),       })   } } 

Nous avons un composant dans l'état duquel est stockée la liste des utilisateurs. Nous téléchargeons cette liste via HTTP à partir d'un certain serveur; la liste est modifiable. Notre composant viole le principe de la responsabilité exclusive, car il a plus d'un motif de changement.

En particulier, je peux voir quatre raisons pour changer un composant. A savoir, le composant change dans les cas suivants:

  • Chaque fois que vous devez changer le titre de l'application.
  • Chaque fois que vous devez ajouter un nouveau composant à l'application (pied de page, par exemple).
  • Chaque fois que vous devez modifier le mécanisme de chargement des données utilisateur, par exemple, l'adresse du serveur ou le protocole.
  • Chaque fois que vous devez modifier le tableau (par exemple, modifiez la mise en forme des colonnes ou effectuez d'autres actions comme celle-ci).

Comment résoudre ces problèmes? Il est nécessaire, après avoir identifié les raisons du changement de composant, d'essayer de les éliminer, de déduire du composant d'origine, de créer des abstractions appropriées (composants ou fonctions) pour chacune de ces raisons.

Nous allons résoudre les problèmes de notre composant d' App en le refactorisant. Son code, après l'avoir divisé en plusieurs composants, ressemblera à ceci:

 class App extends Component {   render() {       return (           <div className="App">               <Header/>               <UserList/>           </div>       );   } } 

Maintenant, si vous devez changer le titre, nous changeons le composant Header - Header , et si vous devez ajouter un nouveau composant à l'application, nous changeons le composant App . Ici, nous avons résolu les problèmes n ° 1 (modification de l'en-tête de l'application) et n ° 2 (ajout d'un nouveau composant à l'application). Cela se fait en déplaçant la logique correspondante du composant App vers les nouveaux composants.

Nous allons maintenant résoudre les problèmes n ° 3 et n ° 4 en créant la classe UserList . Voici son code:

 class UserList extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   state = {       users: [{name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   render() {       return (           <div>               <UserTable users={this.state.users} onUserChange={(user) => this.updateUser(user)}/>               <button onClick={() => this.saveUsers()}>Save</button>           </div>       );   }   updateUser(user) {     //         }   saveUsers(row) {       this.props.saveUsers(this.state.users);   } } 

UserList est notre nouveau composant conteneur. Grâce à lui, nous avons résolu le problème n ° 3 (modification du mécanisme de chargement des utilisateurs) en créant les saveUser propriété fetchUser et saveUser . Par conséquent, maintenant que nous devons changer le lien utilisé pour charger la liste des utilisateurs, nous nous tournons vers la fonction correspondante et y apportons des modifications.

Le dernier problème que nous avons au numéro 4 (modification du tableau qui affiche la liste des utilisateurs) a été résolu en introduisant le composant de présentation UserTable dans le projet, qui encapsule la formation de code HTML et le style du tableau avec les utilisateurs.

Le principe d'ouverture-fermeture (O)


Le principe ouvert fermé stipule que les entités de programme (classes, modules, fonctions) doivent être ouvertes pour l'expansion, mais pas pour la modification.

Si vous regardez le composant UserList décrit ci-dessus, vous remarquerez que si vous devez afficher la liste des utilisateurs dans un format différent, nous devrons modifier la méthode de render de ce composant. Il s'agit d'une violation du principe d'ouverture et de proximité.

Vous pouvez aligner le programme sur ce principe en utilisant la composition des composants .

Jetez un œil au code du composant UserList qui a été refactorisé:

 export class UserList extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   state = {       users: [{id: 1, name: 'Jim', surname: 'Smith', age: 33}]   };   componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   render() {       return (           <div>               {this.props.children({                   users: this.state.users,                   saveUsers: this.saveUsers,                   onUserChange: this.onUserChange               })}           </div>       );   }   saveUsers = () => {       this.props.saveUsers(this.state.users);   };   onUserChange = (user) => {       //         }; } 

Le composant UserList , à la suite de la modification, s'est avéré être ouvert pour l'extension, car il affiche les composants enfants, ce qui facilite un changement dans son comportement. Ce composant est fermé pour modification, car toutes les modifications sont effectuées dans des composants distincts. Nous pouvons même déployer ces composants indépendamment.

Voyons maintenant comment, à l'aide du nouveau composant, une liste d'utilisateurs est affichée.

 export class PopulatedUserList extends Component {   render() {       return (           <div>               <UserList>{                   ({users}) => {                       return <ul>                           {users.map((user, index) => <li key={index}>{user.id}: {user.name} {user.surname}</li>)}                       </ul>                   }               }               </UserList>           </div>       );   } } 

Ici, nous étendons le comportement du composant UserList en créant un nouveau composant qui sait répertorier les utilisateurs. Nous pouvons même télécharger des informations plus détaillées sur chacun des utilisateurs de ce nouveau composant sans toucher au composant UserList , et c'est précisément le but de refactoriser ce composant.

Le principe de substitution de Barbara Lisk (L)


Le principe de substitution de Barbara Liskov (Liskov Substitution Principle) indique que les objets dans les programmes doivent être remplacés par des instances de leurs sous-types sans violer le bon fonctionnement du programme.

Si cette définition vous semble trop librement formulée - en voici une version plus rigoureuse.


Principe de substitution de Barbara Liskov: si quelque chose ressemble à un canard et quacks comme un canard, mais a besoin de piles - probablement la mauvaise abstraction est choisie

Jetez un œil à l'exemple suivant:

 class User { constructor(roles) {   this.roles = roles; } getRoles() {   return this.roles; } } class AdminUser extends User {} const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser({role: 'moderator'},{role: 'admin'}); function showUserRoles(user) { const roles = user.getRoles(); roles.forEach((role) => console.log(role)); } showUserRoles(ordinaryUser); showUserRoles(adminUser); 

Nous avons une classe User dont le constructeur accepte les rôles utilisateur. Sur la base de cette classe, nous créons la classe AdminUser . Après cela, nous avons créé une simple fonction showUserRoles qui prend un objet de type User comme paramètre et affiche tous les rôles attribués à l'utilisateur dans la console.

Nous appelons cette fonction en lui passant ordinaryUser objets ordinaryUser et adminUser , après quoi nous rencontrons une erreur.


Erreur

Qu'est-il arrivé? L'objet de la classe AdminUser similaire à l'objet de la classe User . Il "claque" définitivement en tant User , car il a les mêmes méthodes que l' User . Le problème, ce sont les "batteries". Le fait est que lors de la création de l'objet adminUser , nous lui avons passé quelques objets, pas un tableau.

Ici, le principe de substitution est violé, car la fonction showUserRoles doit fonctionner correctement avec les objets de la classe User et avec les objets créés à partir des classes descendantes de cette classe.

Il n'est pas difficile de AdminUser ce problème - passez simplement AdminUser tableau au constructeur AdminUser au lieu des objets:

 const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']); 

Principe de séparation d'interface (I)


Le principe de séparation des interfaces indique que les programmes ne devraient pas dépendre de ce dont ils n'ont pas besoin.

Ce principe est particulièrement pertinent dans les langages à typage statique, dans lesquels les dépendances sont explicitement définies par des interfaces.

Prenons un exemple:

 class UserTable extends Component {   ...     render() {       const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};       return (           <div>               ...                 <UserRow user={user}/>               ...           </div>       );   }    ... } class UserRow extends Component {   static propTypes = {       user: PropTypes.object.isRequired,   };   render() {       return (           <tr>               <td>Id: {this.props.user.id}</td>               <td>Name: {this.props.user.name}</td>           </tr>       )   } } 

Le composant UserTable UserRow composant UserRow , en lui passant, dans les propriétés, un objet avec des informations utilisateur complètes. Si nous analysons le code du composant UserRow , il s'avère qu'il dépend de l'objet contenant toutes les informations sur l'utilisateur, mais il n'a besoin que des propriétés id et name .

Si vous écrivez un test pour ce composant et utilisez TypeScript ou Flow, vous devrez créer une imitation pour l'objet user avec toutes ses propriétés, sinon le compilateur générera une erreur.

À première vue, cela ne semble pas être un problème si vous utilisez du JavaScript pur, mais si TypeScript s'installe dans votre code, cela entraînera soudainement des échecs de test en raison de la nécessité d'affecter toutes les propriétés des interfaces, même si seulement certaines d'entre elles sont utilisées.

Quoi qu'il en soit, un programme qui satisfait au principe de séparation de l'interface est plus compréhensible.

 class UserTable extends Component {   ...     render() {       const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};       return (           <div>               ...                 <UserRow id={user.id} name={user.name}/>               ...           </div>       );   }    ... } class UserRow extends Component {   static propTypes = {       id: PropTypes.number.isRequired,       name: PropTypes.string.isRequired,   };   render() {       return (           <tr>               <td>Id: {this.props.id}</td>               <td>Name: {this.props.name}</td>           </tr>       )   } } 

N'oubliez pas que ce principe s'applique non seulement aux types de propriété transmis aux composants.

Principe d'inversion de dépendance (D)


Le principe d'inversion de dépendance nous dit que l'objet de la dépendance doit être une abstraction, pas quelque chose de spécifique.

Prenons l'exemple suivant:

 class App extends Component { ... async fetchUsers() {   const users = await fetch('http://totallyhardcodedurl.com/stupid');   this.setState({users}); } ... } 

Si nous analysons ce code, il devient clair que le composant App dépend de la fonction de fetch globale. Si vous décrivez la relation de ces entités dans UML, vous obtenez le diagramme suivant.


Relation entre composant et fonction

Un module de haut niveau ne devrait pas dépendre des implémentations concrètes de bas niveau de quelque chose. Cela devrait dépendre de l'abstraction.

Le composant App n'a pas besoin de savoir comment télécharger les informations utilisateur. Afin de résoudre ce problème, nous devons inverser les dépendances entre le composant App et la fonction fetch . Voici un diagramme UML illustrant cela.


Inversion de dépendance

Voici l'implémentation de ce mécanisme.

 class App extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   ...     componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   ... } 

Nous pouvons maintenant dire que le composant n'est pas bien connecté, car il ne dispose pas d'informations sur le protocole spécifique que nous utilisons - HTTP, SOAP ou tout autre. Le composant ne se soucie pas du tout.

Le respect du principe d'inversion de dépendance élargit nos possibilités de travailler avec du code, car nous pouvons très facilement changer le mécanisme de chargement des données, et le composant App ne changera pas du tout.

De plus, cela simplifie les tests, car il est facile de créer une fonction qui simule la fonction de chargement des données.

Résumé


En investissant du temps dans l'écriture de code de haute qualité, vous gagnerez la gratitude de vos collègues et de vous-même lorsque, à l'avenir, vous devrez faire face à nouveau à ce code. L'intégration des principes SOLID dans le développement d'applications React est un investissement intéressant.

Chers lecteurs! Utilisez-vous des principes SOLID lors du développement d'applications React?

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


All Articles