Recientemente publicamos
material sobre la metodología SOLID. Hoy traemos a su atención una traducción de un artículo que aborda la aplicación de principios SÓLIDOS en el desarrollo de aplicaciones utilizando la popular biblioteca React.

El autor del artículo dice que aquí, por razones de brevedad, no muestra la implementación completa de algunos componentes.
Principio de responsabilidad exclusiva (S)
El principio de responsabilidad única nos dice que un módulo debe tener una y solo una razón para el cambio.
Imagine que estamos desarrollando una aplicación que muestra una lista de usuarios en una tabla. Aquí está el código para el componente de la
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), }) } }
Tenemos un componente en el estado en el que se almacena la lista de usuarios. Descargamos esta lista a través de HTTP desde un servidor determinado; la lista es editable. Nuestro componente viola el principio de responsabilidad exclusiva, ya que tiene más de una razón para el cambio.
En particular, puedo ver cuatro razones para cambiar un componente. A saber, el componente cambia en los siguientes casos:
- Cada vez que necesite cambiar el título de la aplicación.
- Cada vez que necesite agregar un nuevo componente a la aplicación (pie de página, por ejemplo).
- Cada vez que necesite cambiar el mecanismo para cargar los datos del usuario, por ejemplo, la dirección del servidor o el protocolo.
- Cada vez que necesite cambiar la tabla (por ejemplo, cambie el formato de las columnas o realice otras acciones como esta).
¿Cómo resolver estos problemas? Es necesario, después de identificar las razones para cambiar el componente, tratar de eliminarlas, deducir del componente original, creando abstracciones (componentes o funciones) adecuadas para cada una de esas razones.
Resolveremos los problemas de nuestro componente de la
App
refactorizándolo. Su código, después de dividirlo en varios componentes, se verá así:
class App extends Component { render() { return ( <div className="App"> <Header/> <UserList/> </div> ); } }
Ahora, si necesita cambiar el título, cambiamos el componente
Header
, y si necesita agregar un nuevo componente a la aplicación, cambiamos el componente
App
. Aquí solucionamos los problemas No. 1 (cambiar el encabezado de la aplicación) y el problema No. 2 (agregar un nuevo componente a la aplicación). Esto se hace moviendo la lógica correspondiente del componente de la
App
a los nuevos componentes.
Ahora resolveremos los problemas No. 3 y No. 4 creando la clase
UserList
. Aquí está su código:
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
es nuestro nuevo componente contenedor. Gracias a él, resolvimos el problema No. 3 (cambiar el mecanismo de carga del usuario) creando las
saveUser
propiedad
fetchUser
y
saveUser
. Como resultado, ahora que necesitamos cambiar el enlace utilizado para cargar la lista de usuarios, pasamos a la función correspondiente y le hacemos cambios.
El último problema que tenemos en el número 4 (cambiar la tabla que muestra la lista de usuarios) se ha resuelto introduciendo el componente de presentación
UserTable
en el proyecto, que encapsula la formación de código HTML y da estilo a la tabla con los usuarios.
El principio de apertura-cierre (O)
El Principio Abierto Cerrado establece que las entidades del programa (clases, módulos, funciones) deben estar abiertas para la expansión, pero no para la modificación.
Si observa el componente
UserList
descrito anteriormente, notará que si necesita mostrar la lista de usuarios en un formato diferente, tendremos que modificar el método de
render
de este componente. Esto es una violación del principio de apertura-cercanía.
Puede alinear el programa con este principio utilizando la
composición de componentes .
Eche un vistazo al código del componente
UserList
que se ha refactorizado:
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) => {
El componente
UserList
, como resultado de la modificación, resultó estar abierto a la extensión, ya que muestra componentes secundarios, lo que facilita un cambio en su comportamiento. Este componente está cerrado para modificación, ya que todos los cambios se realizan en componentes separados. Incluso podemos implementar estos componentes de forma independiente.
Ahora veamos cómo, usando el nuevo componente, se muestra una lista de usuarios.
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> ); } }
Aquí ampliamos el comportamiento del componente
UserList
creando un nuevo componente que sabe cómo enumerar usuarios. Incluso podemos descargar información más detallada sobre cada uno de los usuarios en este nuevo componente sin tocar el componente
UserList
, y este es precisamente el propósito de refactorizar este componente.
El principio de sustitución de Barbara Lisk (L)
El principio de sustitución de Barbara Liskov (Principio de sustitución de Liskov) indica que los objetos en los programas deben reemplazarse con instancias de sus subtipos sin violar el funcionamiento correcto del programa.
Si esta definición le parece formulada con demasiada libertad, aquí hay una versión más rigurosa de la misma.
Principio de sustitución de Barbara Liskov: si algo parece un pato y grazna como un pato, pero necesita baterías, probablemente se elija la abstracción incorrectaEche un vistazo al siguiente ejemplo:
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);
Tenemos una clase de
User
cuyo constructor acepta roles de usuario. En base a esta clase, creamos la clase
AdminUser
. Después de eso, creamos una función simple
showUserRoles
que toma un objeto de tipo
User
como parámetro y muestra todos los roles asignados al usuario en la consola.
Llamamos a esta función al
adminUser
objetos
ordinaryUser
y
adminUser
, después de lo cual encontramos un error.
ErrorQue paso El objeto de la clase
AdminUser
similar al objeto de la clase
User
. Definitivamente "grazna" como
User
, ya que tiene los mismos métodos que
User
. El problema son las "baterías". El hecho es que al crear el objeto
adminUser
, le pasamos un par de objetos, no una matriz.
Aquí se viola el principio de sustitución, ya que la función
showUserRoles
debería funcionar correctamente con los objetos de la clase
User
y con los objetos creados en base a las clases descendientes de esta clase.
No es difícil
AdminUser
este problema: simplemente pase
AdminUser
matriz al constructor
AdminUser
lugar de los objetos:
const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']);
Principio de separación de interfaz (I)
El principio de segregación de interfaz indica que los programas no deberían depender de lo que no necesitan.
Este principio es especialmente relevante en lenguajes con tipeo estático, en el que las dependencias están explícitamente definidas por las interfaces.
Considere un ejemplo:
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> ) } }
El componente
UserTable
UserRow
componente
UserRow
, pasándolo, en propiedades, un objeto con información completa del usuario. Si analizamos el código del componente
UserRow
, resulta que depende del objeto que contiene toda la información sobre el usuario, pero que solo necesita las propiedades de
id
y
name
.
Si escribe una prueba para este componente y usa TypeScript o Flow, deberá crear una imitación para el objeto de
user
con todas sus propiedades; de lo contrario, el compilador arrojará un error.
A primera vista, esto no parece ser un problema si usa JavaScript puro, pero si TypeScript se instala alguna vez en su código, esto conducirá repentinamente a fallas de prueba debido a la necesidad de asignar todas las propiedades de las interfaces, incluso si solo se usan algunas de ellas.
Sea como fuere, un programa que satisfaga el principio de separación de la interfaz es más comprensible.
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> ) } }
Recuerde que este principio se aplica no solo a los tipos de propiedad pasados a los componentes.
Principio de inversión de dependencia (D)
El principio de inversión de dependencia nos dice que el objeto de la dependencia debe ser una abstracción, no algo específico.
Considere el siguiente ejemplo:
class App extends Component { ... async fetchUsers() { const users = await fetch('http:
Si analizamos este código, queda claro que el componente de la
App
depende de la función de
fetch
global. Si describe la relación de estas entidades en UML, obtendrá el siguiente diagrama.
Relación entre componente y funciónUn módulo de alto nivel no debe depender de implementaciones concretas de bajo nivel de algo. Debería depender de la abstracción.
El componente de la
App
no necesita saber cómo descargar la información del usuario. Para resolver este problema, necesitamos invertir las dependencias entre el componente de la
App
y la función de
fetch
. A continuación se muestra un diagrama UML que ilustra esto.
Inversión de dependenciaAquí está la implementación de este mecanismo.
class App extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; ... componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } ... }
Ahora podemos decir que el componente no está muy conectado, ya que no tiene información sobre qué protocolo usamos: HTTP, SOAP o cualquier otro. El componente no le importa en absoluto.
El cumplimiento del principio de inversión de dependencia amplía nuestras posibilidades para trabajar con código, ya que podemos cambiar fácilmente el mecanismo de carga de datos y el componente de la
App
no cambiará en absoluto.
Además, esto simplifica las pruebas, ya que es fácil crear una función que simule la función de cargar datos.
Resumen
Al invertir tiempo en escribir código de alta calidad, ganará la gratitud de sus colegas y de usted mismo cuando, en el futuro, tenga que enfrentar este código nuevamente. Integrar principios SÓLIDOS en el desarrollo de aplicaciones React es una inversión que vale la pena.
Estimados lectores! ¿Utiliza principios SOLIDOS cuando desarrolla aplicaciones React?
