Aplicando os princípios SOLID no React

Newerton Vargas de Araujo
10 min readSep 15, 2022

--

S.O.L.I.D.

Originally published at https://medium.com/dailyjs/applying-solid-principles-in-react-14905d9c5377

À medida que a indústria de software cresce e comete erros, as melhores práticas e bons princípios de design de software surgem e são conceituados para evitar a repetição dos mesmos erros no futuro. O mundo da programação orientada a objetos (OOP) em particular é uma mina de ouro dessas melhores práticas, e o SOLID é, sem dúvida, um dos mais influentes.

SOLID é uma sigla, onde cada letra representa um dos cinco princípios de design que são:

  • Single responsibility principle (SRP)
  • Open-closed principle (OCP)
  • Liskov substitution principle (LSP)
  • Interface segregation principle (ISP)
  • Dependency inversion principle (DIP)

Neste artigo, falaremos sobre a importância de cada princípio e veremos como podemos aplicar os aprendizados do SOLID em aplicativos React.

Antes de começarmos, porém, há uma grande ressalva. Os princípios SOLID foram concebidos e delineados com a linguagem de programação orientada a objetos em mente. Esses princípios e suas explicações dependem muito de conceitos de classes e interfaces, enquanto o JS também não tem. O que muitas vezes pensamos como “classes” em JS são apenas classes semelhantes usando seu sistema de protótipo, e as interfaces não fazem parte da linguagem (embora a adição do TypeScript ajude um pouco). Ainda mais, a maneira como escrevemos o código React moderno está longe de ser orientada a objetos, parece mais funcional.

A boa notícia, porém, é que os princípios de design de software, como o SOLID, são agnósticos de linguagem e têm um alto nível de abstração, o que significa que, se prestarmos bastante atenção, seremos capazes de aplicá-los ao nosso código React.

Então vamos tomar algumas liberdades.

Single responsibility principle (SRP)

A definição original afirma que “toda classe deve ter apenas uma responsabilidade”, ou seja, fazer exatamente uma coisa. Este princípio é o mais fácil de interpretar, pois podemos simplesmente extrapolar a definição para “toda função/módulo/componente deve fazer exatamente uma coisa”.

De todos os cinco princípios, o SRP é o mais fácil de seguir, mas também é o mais impactante, pois melhora drasticamente a qualidade do nosso código. Para garantir que nossos componentes façam uma coisa, podemos:

quebrar componentes grandes que fazem muito em componentes menores
extrair código não relacionado à funcionalidade do componente principal em funções úteis separadas
encapsular a funcionalidade conectada em hooks personalizados

Agora vamos ver como podemos aplicar este princípio. Começaremos considerando o seguinte componente de exemplo que exibe uma lista de usuários ativos:

Embora este componente seja relativamente curto agora, ele já está fazendo algumas coisas, do tipo buscar dados, filtrar, renderiza o próprio componente e listar individualmente cada usuário. Vamos ver como podemos decompô-lo.

Em primeiro lugar, sempre que conectamos os hooks useState e useEffect, é uma boa oportunidade para extraí-los em um hook customizado:

Agora nosso hook useUsers está preocupado apenas com uma coisa — buscar usuários da API. Também tornou nosso componente principal mais legível, não apenas porque ficou mais curto, mas também porque substituímos os hook estruturais que você precisava para decifrar o propósito por um hook de domínio, cuja finalidade é imediatamente óbvia pelo nome.

Em seguida, vamos ver o JSX que nosso componente renderiza. Sempre que tivermos um mapeamento de loop sobre um array de objetos, devemos prestar atenção na complexidade do JSX que ele produz para itens individuais do array. Se não possui nenhum manipulador de eventos anexado, não há problema em mantê-lo em linha, mas para uma marcação mais complexa, pode ser uma boa ideia extraí-lo em um componente separado:

Assim como em uma alteração anterior, tornamos nosso componente principal menor e mais legível extraindo a lógica para renderizar os itens do usuário em um componente separado.

Por fim, temos a lógica para filtrar usuários inativos da lista de todos os usuários que recebemos de uma API. Essa lógica é relativamente isolada e pode ser reutilizada em outras partes do aplicativo, para que possamos extraí-la facilmente em uma função utilitária:

Neste ponto, nosso componente principal é curto e direto o suficiente para que possamos parar de dividi-lo e encerrar o dia. No entanto, se olharmos um pouco mais de perto, perceberemos que ainda está fazendo mais do que deveria. Atualmente, nosso componente está buscando dados e, em seguida, aplicando filtragem a eles, mas, idealmente, gostaríamos apenas de obter os dados e renderizá-los, sem nenhuma manipulação adicional. Então, como última melhoria, podemos encapsular essa lógica em um novo hook personalizado:

Aqui criamos o hook useActiveUsers para cuidar da lógica de busca e filtragem (também memorizamos dados filtrados para boas medidas), enquanto nosso componente principal é deixado para fazer o mínimo — renderizar os dados que obtém do hook.

Agora, dependendo da nossa interpretação de “falta ainda”, podemos argumentar que o componente ainda está primeiro obtendo os dados e depois os renderizando, o que não é “falta ainda”. Poderíamos dividi-lo ainda mais, chamando um hook em um componente e depois passando o resultado para outro como props, mas encontrei muito poucos casos em que isso é realmente benéfico em aplicativos do mundo real, então vamos perdoar a definição e aceitar “renderização de dados que o componente obtém” como “falta ainda”.

Para resumir, seguindo o princípio de responsabilidade única, efetivamente pegamos um grande pedaço de código monolítico e o tornamos mais modular. A modularidade é ótima porque torna nosso código mais fácil de raciocinar, módulos menores são mais fáceis de testar e modificar, é menos provável que introduzamos duplicação de código não intencional e, como resultado, nosso código se torna mais sustentável.

Deve-se dizer que o que vimos aqui é um exemplo artificial, e em seus próprios componentes você pode descobrir que as dependências entre as diferentes partes móveis são muito mais entrelaçadas. Em muitos casos, isso pode ser uma indicação de más escolhas de design — usando abstrações ruins, criando componentes universais todos-em-um, escopo incorreto dos dados, etc., e portanto, pode ser desembaraçado com uma refatoração mais ampla.

Open-closed principle (OCP)

O OCP afirma que “entidades de software devem ser abertas para extensão, mas fechadas para modificação”. Como nossos componentes e funções do React são entidades de software, não precisamos dobrar a definição e, em vez disso, podemos tomá-la em sua forma original.

O princípio aberto-fechado defende a estruturação de nossos componentes de uma maneira que permita que eles sejam estendidos sem alterar seu código-fonte original. Para vê-lo em ação, vamos considerar o seguinte cenário — estamos trabalhando em um aplicativo que usa um componente Header compartilhado em diferentes páginas e, dependendo da página em que estamos, Header deve renderizar uma interface de usuário ligeiramente diferente:

Aqui renderizamos links para diferentes componentes de página, dependendo da página atual em que estamos. É fácil perceber que essa implementação é ruim se pensarmos no que acontecerá quando começarmos a adicionar mais páginas. Toda vez que uma nova página é criada, precisaremos voltar ao nosso componente Header e ajustar sua implementação para garantir que ele saiba qual link de ação renderizar. Essa abordagem torna nosso componente Header frágil e fortemente acoplado ao contexto em que é usado, e vai contra o princípio aberto-fechado.

Para corrigir esse problema, podemos usar a composição de componentes. Nosso componente Header não precisa se preocupar com o que ele irá renderizar dentro, e ao invés disso, ele pode delegar essa responsabilidade aos componentes que irão usá-lo usando prop children:

Com essa abordagem, removemos completamente a lógica da variável que tínhamos dentro do Header e agora podemos usar a composição para colocar lá literalmente o que quisermos sem modificar o próprio componente. Uma boa maneira de pensar sobre isso é que fornecemos um espaço reservado no componente ao qual podemos nos conectar. E também não estamos limitados a um espaço reservado por componente — se precisarmos ter vários pontos de extensão (ou se a propriedade children já for usado para um propósito diferente), podemos usar qualquer número de props. Se precisarmos passar algum contexto do Header para os componentes que o utilizam, podemos usar o padrão render props. Como você pode ver, a composição pode ser muito poderosa.

Seguindo o princípio aberto-fechado, podemos reduzir o acoplamento entre os componentes e torná-los mais extensíveis e reutilizáveis.

Liskov substitution principle (LSP)

Excessivamente simplificado, LSP pode ser definido como um tipo de relacionamento entre objetos onde “objetos de subtipo devem ser substituíveis por objetos de supertipo”. Esse princípio depende muito da herança de classes para definir relacionamentos de supertipos e subtipos, mas não é muito aplicável em React já que quase nunca lidamos com classes, muito menos com herança de classes. Embora se afastar da herança de classe inevitavelmente transforme esse princípio em algo completamente diferente, escrever código React usando herança seria criar deliberadamente um código ruim (o que a equipe do React desencoraja), então, em vez disso, vamos pular esse princípio.

Interface segregation principle (ISP)

De acordo com o ISP, “os clientes não devem depender de interfaces que não usam”. Por causa das aplicações React, vamos traduzi-lo em “componentes não devem depender de propriedades que eles não usam”.

Estamos esticando a definição do ISP aqui, mas não é muito grande — tanto as propriedades quanto as interfaces podem ser definidos como contratos entre o objeto (componente) e o mundo externo (o contexto em que é usado), para que possamos desenhar paralelos entre os dois. No final das contas, não se trata de ser rígido e inflexível com as definições, mas de aplicar princípios genéricos para resolver um problema.

Para ilustrar melhor o problema que o ISP está direcionando, usaremos o TypeScript para o próximo exemplo. Vamos considerar o aplicativo que renderiza uma lista de vídeos:

Nosso componente Thumbnail que ele usa para cada item pode ser algo assim:

O componente Thumbnail é bem pequeno e simples, mas tem um problema — ele espera que um objeto de vídeo completo seja passado como props, enquanto efetivamente usa apenas uma de suas propriedades.

Para ver por que isso é problemático, imagine que, além dos vídeos, decidamos também exibir miniaturas para transmissões ao vivo, com os dois tipos de recursos de mídia misturados na mesma lista.

Apresentaremos um novo tipo que define um objeto de transmissão ao vivo:

E este é o nosso componente VideoList atualizado:

Como você pode ver, aqui temos um problema. Podemos distinguir facilmente entre objetos de vídeo e transmissão ao vivo, mas não podemos passar o último para o componente Thumbnailporque Videoe LiveStream são incompatíveis. Primeiro, eles têm tipos diferentes, então o TypeScript reclamaria imediatamente. Em segundo lugar, eles contêm o URL da miniatura em propriedades diferentes — o objeto de vídeo o chama de coverUrl, o objeto de transmissão ao vivo o chama de previewUrl. Esse é o problema de ter componentes que dependem de mais propriedades do que realmente precisam — eles se tornam menos reutilizáveis. Então vamos consertar.

Vamos refatorar nosso componente Thumbnail para garantir que ele dependa apenas dos propriedades necessárias:

Com essa alteração, agora podemos usá-la para renderizar miniaturas de vídeos e transmissões ao vivo:

O princípio de segregação de interface defende a minimização das dependências entre os componentes do sistema, tornando-os menos acoplados e, portanto, mais reutilizáveis.

Dependency inversion principle (DIP)

O princípio da inversão de dependência afirma que “devemos depender de abstrações, não de concreções”. Em outras palavras, um componente não deve depender diretamente de outro componente, mas ambos devem depender de alguma abstração comum. Aqui, “componente” refere-se a qualquer parte do nosso aplicativo, seja um componente React, uma função utilitária, um módulo ou uma biblioteca de terceiros. Esse princípio pode ser difícil de entender em abstrato, então vamos direto ao exemplo.

Abaixo temos o componente LoginForm que envia as credenciais do usuário para alguma API quando o formulário é enviado:

Neste trecho de código, nosso componente LoginForm faz referência direta ao módulo api, portanto, há um acoplamento estreito entre eles. Isso é ruim porque essa dependência torna mais desafiador fazer alterações em nosso código, pois uma alteração em um componente afetará outros componentes. O princípio de inversão de dependência defende a quebra desse acoplamento, então vamos ver como podemos conseguir isso.

Primeiro, vamos remover a referência direta ao módulo api de dentro do LoginForme, em vez disso, permitir que a funcionalidade necessária seja injetada por meio de props:

Com essa mudança, nosso componente LoginForm não depende mais do módulo api. A lógica para enviar credenciais para a API é abstraída por meio do retorno de chamada onSubmit e agora é responsabilidade do componente pai fornecer a implementação concreta dessa lógica.

Para isso, criaremos uma versão conectada do LoginForm que delegará a lógica de envio de formulários ao módulo api:

O componente ConnectedLoginForm serve como um conector entre a API e o LoginForm, enquanto eles permanecem totalmente independentes um do outro. Podemos testá-los isoladamente sem nos preocupar em quebrar peças móveis dependentes, pois não há nenhuma. E desde que o LOginForm e a api sigam a abstração comum acordada, o aplicativo como um todo continuará funcionando conforme o esperado.

No passado, essa abordagem de criar componentes de apresentação “burros” e depois injetar lógica neles também era usada por muitas bibliotecas de terceiros. O exemplo mais conhecido disso é o Redux, que vincularia props de retorno de “callback” nos componentes para “despachar” funções usando o componente “conector” de alta ordem (HOC). Com a introdução de hooks esta abordagem tornou-se um pouco menos relevante, mas injetar lógica via HOCs ainda tem utilidade em aplicações React.

Para concluir, o princípio de inversão de dependência visa minimizar o acoplamento entre os diferentes componentes da aplicação. Como você provavelmente notou, minimizar é um tema recorrente em todos os princípios do SOLID — desde minimizar o escopo de responsabilidades para componentes individuais até minimizar o conhecimento de componentes cruzados e as dependências entre eles.

Conclusão

Os princípios SOLID têm sua aplicação muito além disso. Neste artigo, vimos como, tendo alguma flexibilidade nas interpretações desses princípios, conseguimos aplicá-los ao nosso código React e torná-lo mais sustentável e robusto.

É importante lembrar, porém, que ser dogmático e seguir religiosamente esses princípios pode ser prejudicial e levar a um código com engenharia excessiva, portanto, devemos aprender a reconhecer quando a decomposição ou desacoplamento de componentes pode introduzir complexidade ou nenhum benefício.

Originally published at https://medium.com/dailyjs/applying-solid-principles-in-react-14905d9c5377

--

--

Newerton Vargas de Araujo

Software Enginner | Next.js | NestJs | React Native | Flutter | DevOps