Front-end Patterns - Container/Presentational && SRP(Single Responsibility Principle)

O padrão de projeto Container/Presentational, também conhecido como padrão Smart/Dumb ou Controller/View, é uma abordagem comum em desenvolvimento de interfaces de usuário. Ele separa as responsabilidades das componentes em dois tipos principais:

  • Container (Smart/Controller):

    O Container é responsável pela lógica de negócios e pela comunicação com outras partes do sistema. Ele geralmente contém a lógica de estado e coordena as interações entre os componentes.

  • Presentational (Dumb/View):

    O Presentational é responsável pela exibição da interface de usuário e pela interação com o usuário. Ele normalmente recebe os dados do Container e os renderiza na tela, sem ter conhecimento ou dependência direta de outros componentes.

O Container deve ser responsável pela lógica de negócios e pela coordenação das interações entre os componentes. Ele não deve se preocupar com a apresentação da interface de usuário, pois essa é a responsabilidade do Presentational.

Por outro lado, o Presentational deve ser responsável apenas pela exibição dos dados fornecidos pelo Container e pela interação com o usuário. Ele não deve conter lógica de negócios complexa ou coordenar interações entre componentes.

E qual a relação desse padrão com o SOLID? 🧐

Princípio da Responsabilidade Única - (Single Responsibility Principle)


Cada classe ou módulo deve ter uma única responsabilidade e razão para mudar. Isso significa que uma classe deve ter apenas uma única tarefa ou funcionalidade, de modo que, se houver a necessidade de fazer uma alteração nessa funcionalidade, ela afetará apenas uma única classe.

O princípio da Responsabilidade Única do SOLID está relacionado a esse padrão porque ele enfatiza que cada classe ou componente deve ter uma única responsabilidade. No contexto do Container/Presentational, isso significa que o Container e o Presentational devem ter responsabilidades bem definidas e separadas.

Vamos a um exemplo no Reactjs 🏃🏾


Nesse exemplo tenho apenas um custom-hook, que nada mais é do que uma função que exporta uma solicitação HTTP para a API que por sua vez me devolve 6 Labradores fofos🐶💛:

  • 👈🏾 Explicação detalhada

    Esse componente personalizado chamado useDogImages é uma função React que retorna um estado dogs e possui um efeito colateral utilizando o hook useEffect. O estado dogs é inicializado com um array vazio através do useState.

    O useEffect é usado para executar o código dentro dele após a renderização do componente. Nesse caso, o código realiza uma solicitação HTTP para a API "https://dog.ceo/api/breed/labrador/images/random/6" usando a função fetch. Essa API retorna um conjunto de 6 imagens aleatórias de cães da raça Labrador.

    Quando a resposta da API é recebida, o código chama res.json() para converter a resposta em formato JSON. Em seguida, a propriedade message do objeto JSON é extraída e usada para atualizar o estado dogs chamando setDogs(message).

    O efeito colateral é configurado para ser executado apenas uma vez, passando um array vazio [] como segundo argumento para o useEffect. Isso garante que a solicitação HTTP seja feita apenas na primeira renderização do componente.

    Por fim, a função useDogImages retorna o estado dogs, que é o resultado das imagens dos cães obtidos da API.

import { useEffect, useState } from "react";

export default function useDogImages() {
  const [dogs, setDogs] = useState([]);

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then((res) => res.json())
      .then(({ message }) => setDogs(message));
  }, []);

  return dogs;
}

E sem me preocupar com o css tenho App.js:

import "./styles.css";
import useDogImages from "./useDogImages";

export default function App() {
  const dogs = useDogImages();
  return (
    <div className="App">
      <h2>Dogs Fofinhos</h2>
        {
           dogs.map((dog, i) => (
              <img src={dog} key={i} alt="Dog" />
	   ))
	}
    </div>
  );
}

Ao aderir ao Princípio da Responsabilidade Única, o código se torna mais modular, reutilizável e de fácil manutenção, permitindo que o Container e o Presentational evoluam independentemente um do outro, facilitando a manutenção e extensão do sistema como um todo.

Portanto, ao aplicar o padrão Container/Presentational, você está seguindo o SRP, pois está dividindo as responsabilidades entre os componentes de forma coesa. No entanto, o SRP é um princípio mais amplo que pode ser aplicado em outros contextos além do padrão Container/Presentational.

Evoluindo com Injeção de Dependência


Injeção de dependência (DI - Dependency Injection) é um padrão de projeto de software que permite que objetos dependam uns dos outros sem criar acoplamento rígido entre eles. Em vez disso, os objetos são injetados com suas dependências por meio de um contêiner de injeção de dependência. Isso torna o código mais modular e fácil de testar.

vamos utilizar nosso código para evoluir utilizando DI:

Primeiro, criaremos um novo componente chamado ApiService para encapsular a lógica de requisição HTTP. Esse componente será responsável por realizar a chamada à API e retornar os dados obtidos. Veja como ficaria o código:

export default class ApiService {
  async fetchDogImages() {
    const response = await fetch("https://dog.ceo/api/breed/labrador/images/random/6");
    const { message } = await response.json();
    return message;
  }
}

Neste exemplo, o ApiService possui um método fetchDogImages que realiza a requisição HTTP e retorna os dados obtidos. Essa classe segue o princípio da responsabilidade única ao ter a única responsabilidade de lidar com a comunicação com a API.

Agora, podemos modificar o hook useDogImages para receber uma instância do ApiService como dependência. Dessa forma, estamos injetando a dependência externa no hook. Veja o código atualizado:

import { useEffect, useState } from "react";

export default function useDogImages(apiService) {
  const [dogs, setDogs] = useState([]);

  useEffect(() => {
    apiService.fetchDogImages().then((message) => setDogs(message));
  }, []);

  return dogs;
}

No hook useDogImages, utilizamos o método fetchDogImages do ApiService para buscar as imagens dos cachorros. Note que agora não há mais a dependência direta do fetch padrão do navegador.

Por fim, no App.js, vamos criar uma instância do ApiService e passá-la para o useDogImages como dependência:

import "./styles.css";
import useDogImages from "./useDogImages";
import ApiService from "./ApiService";

export default function App() {
  const apiService = new ApiService();
  const dogs = useDogImages(apiService);

  return (
    <div className="App">
      <h2>Dogs Fofinhos</h2>
      {dogs.map((dog, i) => (
        <img src={dog} key={i} alt="Dog" />
      ))}
    </div>
  );
}

Agora, o App cria uma instância do ApiService e a passa como dependência para o useDogImages. Dessa forma, estamos separando as responsabilidades: o ApiService é responsável pela comunicação com a API, e o useDogImages apenas utiliza esse serviço para obter as imagens dos cachorros.

Você pode evoluir ainda mais e fazer a Inversão de Dependência!

Mais meu objetivo chegou ao fim! 🔚

Com essa abordagem esporo ter te inspirado a procurar essa integração de um princípio do SOLID com um padrão de projeto bem popular, e se você já conhecia e usava, parabéns, esse é o caminho da evolução.

Primeiramente, parabéns pelo tópico. Sempre ouvia falar sobre injeção de dependência mas nunca tinha parado pra ler. O exemplo é bem didático também.

Mas fiquei com uma dúvida, qual a diferença, neste exemplo, do fetch isolado na classe? Se houver problema na requisição, afetará o hook e o app de qualquer maneira. Num primeiro momento me parece até overengineering. Mas pergunto aqui justamente pela ignorância no assunto.

Obrigado pelo comentário! A ideia aqui é isolar e tratar os componentes adequadamente. Se houver problema na requisição é problema do endpoint, certo, e o componente não tem nada com isso. E eu falei em `ISOLAR`? Bom, em tempos de garantir o funcionamento de cada parte da aplicação, é bem mais fácil e prático `testar` as partes sem muita `dependência`. Nesse caso você pode trocar o **fetch** pelo **Axios** sem problemas e a App não vai nem saber. Com esse desacomplamento, você não fica refém do fetch ou do Axios. Esse foi o exemplo mais simples e fácil de entender, e a injeção de dependência foi um plus, você pode procurar a teoria por trás, ou um exemplo mais completo.
> Nesse caso você pode trocar o fetch pelo Axios sem problemas e a App não vai nem saber. Com esse desacomplamento, você não fica refém do fetch ou do Axios. Agora fez sentido, valeu!