Aplicación de principios SÓLIDOS para reaccionar al desarrollo de aplicaciones

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.

imagen

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) {     //         }   saveUsers(row) {       this.props.saveUsers(this.state.users);   } } 

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 incorrecta

Eche 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.


Error

Que 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://totallyhardcodedurl.com/stupid');   this.setState({users}); } ... } 

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ón

Un 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 dependencia

Aquí 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?

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


All Articles