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.

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) {
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 choisieJetez 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.
ErreurQu'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:
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 fonctionUn 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épendanceVoici 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?
