Aplicando princípios do SOLID para reagir ao desenvolvimento de aplicativos

Publicamos recentemente material sobre a metodologia SOLID. Hoje, chamamos a atenção para a tradução de um artigo que aborda a aplicação dos princípios do SOLID no desenvolvimento de aplicativos usando a popular biblioteca React.

imagem

O autor do artigo diz que aqui, por uma questão de brevidade, ele não mostra a implementação completa de alguns componentes.

Princípio da (s) responsabilidade (s)


O Princípio da Responsabilidade Única nos diz que um módulo deve ter um e apenas um motivo para a mudança.

Imagine que estamos desenvolvendo um aplicativo que exibe uma lista de usuários em uma tabela. Aqui está o código para o componente 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),       })   } } 

Temos um componente no estado em que a lista de usuários está armazenada. Fazemos o download dessa lista via HTTP de um determinado servidor; a lista é editável. Nosso componente viola o princípio de responsabilidade exclusiva, pois possui mais de um motivo para mudança.

Em particular, posso ver quatro razões para alterar um componente. Ou seja, o componente muda nos seguintes casos:

  • Sempre que você precisar alterar o título do aplicativo.
  • Sempre que você precisar adicionar um novo componente ao aplicativo (rodapé da página, por exemplo).
  • Sempre que você precisar alterar o mecanismo para carregar dados do usuário, por exemplo, endereço ou protocolo do servidor.
  • Sempre que você precisar alterar a tabela (por exemplo, altere a formatação das colunas ou execute outras ações como esta).

Como resolver esses problemas? É necessário, após identificar os motivos da alteração do componente, tentar eliminá-los, deduzir do componente original, criando abstrações adequadas (componentes ou funções) para cada um desses motivos.

Resolveremos os problemas do nosso componente de App refatorando-o. Seu código, depois de dividi-lo em vários componentes, ficará assim:

 class App extends Component {   render() {       return (           <div className="App">               <Header/>               <UserList/>           </div>       );   } } 

Agora, se você precisar alterar o título, Header componente Header e, se precisar adicionar um novo componente ao aplicativo, alteramos o componente App . Aqui, resolvemos os problemas nº 1 (alterando o cabeçalho do aplicativo) e o problema nº 2 (adicionando um novo componente ao aplicativo). Isso é feito movendo a lógica correspondente do componente App para os novos componentes.

Agora, resolveremos os problemas nº 3 e nº 4 criando a classe UserList . Aqui está o código dele:

 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 é o nosso novo componente de contêiner. Graças a ele, resolvemos o problema nº 3 (alterando o mecanismo de carregamento do usuário) criando as saveUser propriedade saveUser e saveUser . Como resultado, agora que precisamos alterar o link usado para carregar a lista de usuários, passamos à função correspondente e fazemos alterações nela.

O último problema que temos no número 4 (alterando a tabela que exibe a lista de usuários) foi resolvido introduzindo o componente de apresentação UserTable no projeto, que encapsula a formação do código HTML e estiliza a tabela com os usuários.

O princípio do fechamento-abertura (O)


O Princípio Aberto Fechado afirma que as entidades do programa (classes, módulos, funções) devem estar abertas para expansão, mas não para modificação.

Se você observar o componente UserList descrito acima, notará que se precisar exibir a lista de usuários em um formato diferente, teremos que modificar o método de render desse componente. Isso é uma violação do princípio da abertura-proximidade.

Você pode alinhar o programa com esse princípio usando a composição dos componentes .

Dê uma olhada no código do componente UserList que foi refatorado:

 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) => {       //         }; } 

O componente UserList , como resultado da modificação, acabou sendo aberto para extensão, pois exibe componentes filhos, o que facilita uma mudança no seu comportamento. Este componente está fechado para modificação, pois todas as alterações são executadas em componentes separados. Podemos até implantar esses componentes de forma independente.

Agora vamos ver como, usando o novo componente, uma lista de usuários é exibida.

 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>       );   } } 

Aqui, estendemos o comportamento do componente UserList criando um novo componente que sabe como listar usuários. Podemos até baixar informações mais detalhadas sobre cada um dos usuários desse novo componente, sem tocar no componente UserList , e esse foi precisamente o objetivo de refatorar esse componente.

O princípio da substituição de Barbara Lisk (L)


O princípio de substituição de Barbara Liskov (Princípio de Substituição de Liskov) indica que os objetos nos programas devem ser substituídos por instâncias de seus subtipos sem violar a operação correta do programa.

Se essa definição lhe parece muito livremente formulada - aqui está uma versão mais rigorosa dela.


Princípio da substituição de Barbara Liskov: se algo se parece com um pato e grasna como um pato, mas precisa de baterias - provavelmente a abstração errada é escolhida

Veja o seguinte exemplo:

 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); 

Temos uma classe User cujo construtor aceita funções de usuário. Com base nessa classe, criamos a classe AdminUser . Depois disso, criamos uma função showUserRoles simples que usa um objeto do tipo User como parâmetro e exibe todas as funções atribuídas ao usuário no console.

Chamamos essa função passando objetos de adminUser ordinaryUser e adminUser para ele, após o qual encontramos um erro.


Erro

O que aconteceu O objeto da classe AdminUser semelhante ao objeto da classe User . Definitivamente "grasna" como User , uma vez que possui os mesmos métodos que o User . O problema são as "baterias". O fato é que, ao criar o objeto adminUser , passamos alguns objetos para ele, não uma matriz.

Aqui, o princípio da substituição é violado, pois a função showUserRoles deve funcionar corretamente com objetos da classe User e com objetos criados com base nas classes descendentes dessa classe.

Não é difícil AdminUser esse problema - basta passar AdminUser matriz para o construtor AdminUser vez de objetos:

 const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']); 

Princípio de separação de interface (I)


O Princípio de Segregação de Interface indica que os programas não devem depender do que não precisam.

Esse princípio é especialmente relevante em linguagens com tipagem estática, nas quais as dependências são explicitamente definidas pelas interfaces.

Considere um exemplo:

 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>       )   } } 

O componente UserTable UserRow componente UserRow , passando-o, nas propriedades, para um objeto com informações completas do usuário. Se analisarmos o código do componente UserRow , verifica-se que depende do objeto que contém todas as informações sobre o usuário, mas ele precisa apenas das propriedades id e name .

Se você escrever um teste para este componente e usar TypeScript ou Flow, precisará criar uma imitação para o objeto de user com todas as suas propriedades; caso contrário, o compilador gerará um erro.

À primeira vista, isso não parece ser um problema se você usar JavaScript puro, mas se o TypeScript se estabelecer no seu código, isso levará a falhas de teste devido à necessidade de atribuir todas as propriedades das interfaces, mesmo que apenas algumas delas sejam usadas.

Seja como for, um programa que satisfaça o princípio de separação da interface é mais compreensível.

 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>       )   } } 

Lembre-se de que esse princípio se aplica não apenas aos tipos de propriedades passados ​​para os componentes.

Princípio de Inversão de Dependência (D)


O Princípio da Inversão da Dependência nos diz que o objeto da dependência deve ser uma abstração, não algo específico.

Considere o seguinte exemplo:

 class App extends Component { ... async fetchUsers() {   const users = await fetch('http://totallyhardcodedurl.com/stupid');   this.setState({users}); } ... } 

Se analisarmos esse código, fica claro que o componente App depende da função de fetch global. Se você descrever o relacionamento dessas entidades na UML, obterá o diagrama a seguir.


Relação entre componente e função

Um módulo de alto nível não deve depender de implementações concretas de baixo nível de algo. Deve depender da abstração.

O componente App não precisa saber como baixar informações do usuário. Para resolver esse problema, precisamos inverter as dependências entre o componente App e a função de fetch . Abaixo está um diagrama UML que ilustra isso.


Inversão de Dependência

Aqui está a implementação deste mecanismo.

 class App extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   ...     componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   ... } 

Agora podemos dizer que o componente não está muito conectado, pois não possui informações sobre qual protocolo usamos - HTTP, SOAP ou qualquer outro. O componente não se importa.

A conformidade com o princípio da inversão de dependência expande nossas possibilidades de trabalhar com código, pois podemos alterar muito facilmente o mecanismo de carregamento de dados e o componente App não será alterado.

Além disso, isso simplifica o teste, pois é fácil criar uma função que simula a função de carregar dados.

Sumário


Ao investir tempo em escrever códigos de alta qualidade, você ganhará a gratidão de seus colegas e de si mesmo quando, no futuro, precisar enfrentar esse código novamente. A integração dos princípios do SOLID no desenvolvimento do aplicativo React é um investimento que vale a pena.

Caros leitores! Você usa os princípios do SOLID ao desenvolver aplicativos React?

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


All Articles