Dans ce blog, je vais démontrer la mise en œuvre des principes SOLID dans une application React. À la fin de cet article, vous maîtriserez pleinement les principes SOLID. Avant de commencer, permettez-moi de vous donner une brève introduction à ces principes.
Les principes SOLID sont cinq principes de conception qui nous aident à garder notre application réutilisable, maintenable, évolutive et faiblement couplée. Les principes SOLID sont :
Bon, examinons chacun de ces principes individuellement. J'utilise React comme exemple, mais les concepts de base sont similaires à d'autres langages de programmation et frameworks.
"Un module doit être responsable devant un, et un seul, acteur." - Wikipédia.
Le principe de responsabilité unique stipule qu'un composant doit avoir un objectif ou une responsabilité clairs. Il doit se concentrer sur une fonctionnalité ou un comportement spécifique et éviter d'entreprendre des tâches sans rapport. Suivre SRP rend les composants plus ciblés, modulaires et faciles à comprendre et à modifier. Voyons la mise en œuvre réelle.
// ❌ Bad Practice: Component with Multiple Responsibilities const Products = () => { return ( <div className="products"> {products.map((product) => ( <div key={product?.id} className="product"> <h3>{product?.name}</h3> <p>${product?.price}</p> </div> ))} </div> ); };
Dans l'exemple ci-dessus, le composant Products
enfreint le principe de responsabilité unique en assumant plusieurs responsabilités. Il gère l'itération des produits et gère le rendu de l'interface utilisateur pour chaque produit. Cela peut rendre le composant difficile à comprendre, à maintenir et à tester à l'avenir.
// ✅ Good Practice: Separating Responsibilities into Smaller Components import Product from './Product'; import products from '../../data/products.json'; const Products = () => { return ( <div className="products"> {products.map((product) => ( <Product key={product?.id} product={product} /> ))} </div> ); }; // Product.js // Separate component responsible for rendering the product details const Product = ({ product }) => { return ( <div className="product"> <h3>{product?.name}</h3> <p>${product?.price}</p> </div> ); };
Cette séparation garantit que chaque composant a une responsabilité unique, ce qui les rend plus faciles à comprendre, à tester et à entretenir.
"Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification." - Wikipédia.
Le principe ouvert-fermé souligne que les composants doivent être ouverts pour l'extension (peut ajouter de nouveaux comportements ou fonctionnalités) mais fermés pour modification (le code existant doit rester inchangé). Ce principe encourage la création de code résilient au changement, modulaire et facilement maintenable. Voyons la mise en œuvre réelle.
// ❌ Bad Practice: Violating the Open-Closed Principle // Button.js // Existing Button component const Button = ({ text, onClick }) => { return ( <button onClick={onClick}> {text} </button> ); } // Button.js // Modified Existing Button component with additional icon prop (modification) const Button = ({ text, onClick, icon }) => { return ( <button onClick={onClick}> <i className={icon} /> <span>{text}</span> </button> ); } // Home.js // 👇 Avoid: Modified existing component prop const Home = () => { const handleClick= () => {}; return ( <div> {/* ❌ Avoid this */} <Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" /> </div> ); }
Dans l'exemple ci-dessus, nous modifions le composant Button
existant en ajoutant un accessoire icon
. La modification d'un composant existant pour répondre à de nouvelles exigences enfreint le principe ouvert-fermé. Ces changements fragilisent le composant et introduisent le risque d'effets secondaires involontaires lorsqu'il est utilisé dans différents contextes.
// ✅ Good Practice: Open-Closed Principle // Button.js // Existing Button functional component const Button = ({ text, onClick }) => { return ( <button onClick={onClick}> {text} </button> ); } // IconButton.js // IconButton component // ✅ Good: You have not modified anything here. const IconButton = ({ text, icon, onClick }) => { return ( <button onClick={onClick}> <i className={icon} /> <span>{text}</span> </button> ); } const Home = () => { const handleClick = () => { // Handle button click event } return ( <div> <Button text="Submit" onClick={handleClick} /> {/* <IconButton text="Submit" icon="fas fa-heart" onClick={handleClick} /> </div> ); }
Dans l'exemple ci-dessus, nous créons un composant fonctionnel IconButton
séparé. Le composant IconButton
encapsule le rendu d'un bouton d'icône sans modifier le composant Button
existant. Il adhère au principe ouvert-fermé en étendant la fonctionnalité par la composition plutôt que par la modification.
"Les objets de sous-type doivent être substituables aux objets de supertype" - Wikipedia.
Le principe de substitution de Liskov (LSP) est un principe fondamental de la programmation orientée objet qui met l'accent sur la nécessité de substituabilité des objets au sein d'une hiérarchie. Dans le contexte des composants React, LSP promeut l'idée que les composants dérivés devraient pouvoir remplacer leurs composants de base sans affecter l'exactitude ou le comportement de l'application. Voyons la mise en œuvre réelle.
// ⚠️ Bad Practice // This approach violates the Liskov Substitution Principle as it modifies // the behavior of the derived component, potentially resulting in unforeseen // problems when substituting it for the base Select component. const BadCustomSelect = ({ value, iconClassName, handleChange }) => { return ( <div> <i className={iconClassName}></i> <select value={value} onChange={handleChange}> <options value={1}>One</options> <options value={2}>Two</options> <options value={3}>Three</options> </select> </div> ); }; const LiskovSubstitutionPrinciple = () => { const [value, setValue] = useState(1); const handleChange = (event) => { setValue(event.target.value); }; return ( <div> {/** ❌ Avoid this */} {/** Below Custom Select doesn't have the characteristics of base `select` element */} <BadCustomSelect value={value} handleChange={handleChange} /> </div> );
Dans l'exemple ci-dessus, nous avons un composant BadCustomSelect
destiné à servir d'entrée de sélection personnalisée dans React. Cependant, il viole le principe de substitution de Liskov (LSP) car il limite le comportement de l'élément select
de base.
// ✅ Good Practice // This component follows the Liskov Substitution Principle and allows the use of select's characteristics. const CustomSelect = ({ value, iconClassName, handleChange, ...props }) => { return ( <div> <i className={iconClassName}></i> <select value={value} onChange={handleChange} {...props}> <options value={1}>One</options> <options value={2}>Two</options> <options value={3}>Three</options> </select> </div> ); }; const LiskovSubstitutionPrinciple = () => { const [value, setValue] = useState(1); const handleChange = (event) => { setValue(event.target.value); }; return ( <div> {/* ✅ This CustomSelect component follows the Liskov Substitution Principle */} <CustomSelect value={value} handleChange={handleChange} defaultValue={1} /> </div> ); };
Dans le code révisé, nous avons un composant CustomSelect
destiné à étendre les fonctionnalités de l'élément select
standard dans React. Le composant accepte des accessoires tels que value
, iconClassName
, handleChange
et des accessoires supplémentaires à l'aide de l'opérateur de propagation ...props
. En permettant l'utilisation des caractéristiques de l'élément select
et en acceptant des accessoires supplémentaires, le composant CustomSelect
suit le principe de substitution de Liskov (LSP).
"Aucun code ne devrait être contraint de dépendre de méthodes qu'il n'utilise pas." - Wikipédia.
Le principe de ségrégation des interfaces (ISP) suggère que les interfaces doivent être ciblées et adaptées aux besoins spécifiques des clients plutôt que d'être trop larges et d'obliger les clients à mettre en œuvre des fonctionnalités inutiles. Voyons la mise en œuvre réelle.
// ❌ Avoid: disclose unnecessary information for this component // This introduces unnecessary dependencies and complexity for the component const ProductThumbnailURL = ({ product }) => { return ( <div> <img src={product.imageURL} alt={product.name} /> </div> ); }; // ❌ Bad Practice const Products = ({ product }) => { return ( <div> <ProductThumbnailURL product={product} /> <h4>{product?.name}</h4> <p>{product?.description}</p> <p>{product?.price}</p> </div> ); }; const Products = () => { return ( <div> {products.map((product) => ( <Product key={product.id} product={product} /> ))} </div> ); }
Dans l'exemple ci-dessus, nous transmettons tous les détails du produit au composant ProductThumbnailURL
, même s'il ne l'exige pas. Cela ajoute des risques et une complexité inutiles au composant et enfreint le principe de séparation des interfaces (ISP).
// ✅ Good: reducing unnecessary dependencies and making // the codebase more maintainable and scalable. const ProductThumbnailURL = ({ imageURL, alt }) => { return ( <div> <img src={imageURL} alt={alt} /> </div> ); }; // ✅ Good Practice const Products = ({ product }) => { return ( <div> <ProductThumbnailURL imageURL={product.imageURL} alt={product.name} /> <h4>{product?.name}</h4> <p>{product?.description}</p> <p>{product?.price}</p> </div> ); }; const Products = () => { return ( <div> {products.map((product) => ( <Product key={product.id} product={product} /> ))} </div> ); };
Dans le code révisé, le composant ProductThumbnailURL
ne reçoit que les informations requises au lieu de tous les détails du produit. Il prévient les risques inutiles et favorise le principe de ségrégation des interfaces (ISP).
"Une entité devrait dépendre d'abstractions, pas de concrétions" - Wikipedia.
Le principe d'inversion de dépendance (DIP) souligne que les composants de haut niveau ne doivent pas dépendre des composants de bas niveau. Ce principe favorise le couplage lâche et la modularité et facilite la maintenance des systèmes logiciels. Voyons la mise en œuvre réelle.
// ❌ Bad Practice // This component follows concretion instead of abstraction and // breaks Dependency Inversion Principle const CustomForm = ({ children }) => { const handleSubmit = () => { // submit operations }; return <form onSubmit={handleSubmit}>{children}</form>; }; const DependencyInversionPrinciple = () => { const [email, setEmail] = useState(); const handleChange = (event) => { setEmail(event.target.value); }; const handleFormSubmit = (event) => { // submit business logic here }; return ( <div> {/** ❌ Avoid: tightly coupled and hard to change */} <BadCustomForm> <input type="email" value={email} onChange={handleChange} name="email" /> </BadCustomForm> </div> ); };
Le composant CustomForm
est étroitement couplé à ses enfants, ce qui empêche la flexibilité et rend difficile la modification ou l'extension de son comportement.
// ✅ Good Practice // This component follows abstraction and promotes Dependency Inversion Principle const AbstractForm = ({ children, onSubmit }) => { const handleSubmit = (event) => { event.preventDefault(); onSubmit(); }; return <form onSubmit={handleSubmit}>{children}</form>; }; const DependencyInversionPrinciple = () => { const [email, setEmail] = useState(); const handleChange = (event) => { setEmail(event.target.value); }; const handleFormSubmit = () => { // submit business logic here }; return ( <div> {/** ✅ Use the abstraction instead */} <AbstractForm onSubmit={handleFormSubmit}> <input type="email" value={email} onChange={handleChange} name="email" /> <button type="submit">Submit</button> </AbstractForm> </div> ); };
Dans le code révisé, nous introduisons le composant AbstractForm
, qui agit comme une abstraction pour le formulaire. Il reçoit la fonction onSubmit
en tant que prop et gère la soumission du formulaire. Cette approche nous permet d'échanger ou d'étendre facilement le comportement du formulaire sans modifier le composant de niveau supérieur.
Les principes SOLID fournissent des lignes directrices qui permettent aux développeurs de créer des solutions logicielles bien conçues, maintenables et extensibles. En adhérant à ces principes, les développeurs peuvent atteindre la modularité, la réutilisabilité du code, la flexibilité et la complexité réduite du code.
J'espère que ce blog vous a fourni des informations précieuses et vous a inspiré à appliquer ces principes dans vos projets React existants ou à venir.
Reste curieux; continuez à coder !
Référence:
Également publié ici .