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 estadodogs
e possui um efeito colateral utilizando o hookuseEffect
. O estadodogs
é inicializado com um array vazio através douseState
.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çãofetch
. 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 propriedademessage
do objeto JSON é extraída e usada para atualizar o estadodogs
chamandosetDogs(message)
.O efeito colateral é configurado para ser executado apenas uma vez, passando um array vazio
[]
como segundo argumento para ouseEffect
. Isso garante que a solicitação HTTP seja feita apenas na primeira renderização do componente.Por fim, a função
useDogImages
retorna o estadodogs
, 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.