“Un módulo debe ser responsable ante un solo actor”. —Wikipedia.
// ❌ 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> ); };
En el ejemplo anterior, el componente Products
viola el Principio de responsabilidad única al asumir múltiples responsabilidades. Administra la iteración de productos y maneja la representación de la interfaz de usuario para cada producto. Esto puede hacer que el componente sea difícil de entender, mantener y probar en el futuro.
// ✅ 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> ); };
"Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación". —Wikipedia.
// ❌ 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> ); }
En el ejemplo anterior, modificamos el componente Button
existente agregando un accesorio icon
. Alterar un componente existente para acomodar nuevos requisitos viola el Principio Abierto-Cerrado. Estos cambios hacen que el componente sea más frágil e introducen el riesgo de efectos secundarios no deseados cuando se usan en diferentes contextos.
// ✅ 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> ); }
En el ejemplo anterior, creamos un componente funcional IconButton
separado. El componente IconButton
encapsula la representación de un botón de icono sin modificar el componente Button
existente. Se adhiere al Principio Abierto-Cerrado al extender la funcionalidad a través de la composición en lugar de la modificación.
“Los objetos de subtipo deben ser sustituibles por objetos de supertipo” — Wikipedia.
// ⚠️ 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> );
En el ejemplo anterior, tenemos un componente BadCustomSelect
destinado a servir como una entrada de selección personalizada en React. Sin embargo, viola el principio de sustitución de Liskov (LSP) porque restringe el comportamiento del elemento select
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> ); };
En el código revisado, tenemos un componente CustomSelect
destinado a ampliar la funcionalidad del elemento select
estándar en React. El componente acepta accesorios como value
, iconClassName
, handleChange
y accesorios adicionales mediante el operador de propagación ...props
. Al permitir el uso de las características del elemento select
y aceptar accesorios adicionales, el componente CustomSelect
sigue el principio de sustitución de Liskov (LSP).
“Ningún código debe verse obligado a depender de métodos que no utiliza”. —Wikipedia.
// ❌ 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> ); }
En el ejemplo anterior, pasamos todos los detalles del producto al componente ProductThumbnailURL
, aunque no lo requiera. Agrega riesgos y complejidad innecesarios al componente y viola el Principio de segregación de interfaz (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> ); };
En el código revisado, el componente ProductThumbnailURL
solo recibe la información requerida en lugar de los detalles completos del producto. Previene riesgos innecesarios y fomenta el Principio de Segregación de Interfaz (ISP).
“Una entidad debe depender de abstracciones, no de concreciones” — Wikipedia.
// ❌ 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> ); };
El componente CustomForm
está estrechamente acoplado a sus elementos secundarios, lo que impide la flexibilidad y dificulta cambiar o ampliar su comportamiento.
// ✅ 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> ); };
En el código revisado, presentamos el componente AbstractForm
, que actúa como una abstracción del formulario. Recibe la función onSubmit
como accesorio y maneja el envío del formulario. Este enfoque nos permite intercambiar o extender fácilmente el comportamiento del formulario sin modificar el componente de nivel superior.
Mantente curioso; sigue codificando!
Referencia:
También publicado .