Dans cet article, je vais vous présenter le Design Pattern Container / Presentationnal au travers d’un exemple. Nous verrons à quoi sert ce design pattern et quels sont ses avantages.
Vous pouvez retrouver le projet de démonstration à l’adresse suivante : 🔗 https://github.com/Delmotte-Vincent/container-presentational-pattern-demo
Container / Presentational Pattern
Le but d’un design pattern est de répondre à un problème fréquent de programmation. Le design pattern container / presentational répond à une problématique rencontré dans le développement frontend React en particulier.
Le problème
Il est courant, lorsqu’on construit une interface utilisateur, de devoir faire des appels à une API afin de récupérer des données pour les afficher à l’écran. Il est également courant de se retrouver avec des composants trop grands et illisibles car les appels réseaux, la logique de données et l’affichage sont gérés au même endroit.
Prenons un exemple simple. Vous voulez afficher une liste de Pokémon que vous récupérez d’une API.
Pour cela nous pouvons faire un composant Pokemon comme ci-dessous:
import { useState, useEffect } from 'react';
export default function Pokemon() {
const [pokemons, setPokemons] = useState(null);
useEffect(() => {
const fetchPokemons = async () => {
const pokemonsLinks = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
.then((response) => response.json())
.then((response) => response.results);
const pokemons = await Promise.all(
pokemonsLinks.map((pokemon) => fetch(pokemon.url).then((response) => response.json()))
);
setPokemons(pokemons);
};
fetchPokemons().catch(console.error);
}, []);
if (!pokemons) return null;
return (
<div className="pokemon-list">
{pokemons.map((pokemon, index) => (
<div key={index} className="card-container">
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<div className="description-container">
<div className="pokemon-description">
<h1>{pokemon.name}</h1>
<div className="types">
{pokemon.types.map((type, index) => (
<div className="type-chip" key={index}>
{type.type.name}
</div>
))}
</div>
</div>
<div className="stats">
<p>hp : {pokemon.stats[0].base_stat}</p>
<p>attack : {pokemon.stats[1].base_stat}</p>
<p>defense : {pokemon.stats[2].base_stat}</p>
</div>
</div>
</div>
))}
</div>
);
}
Ce composant ne fait pas beaucoup de chose mais il est déjà composé de 50 lignes. Imaginez un composant qui gère beaucoup plus de données et dont la vue est plus complexe. On arrive vite à un fichier illisible.
La solution
Le design pattern container / presentational, comme son nom l’indique, propose de séparer les différentes opérations qu’un composant effectue. En effet, un composant peut effectuer deux actions : récupérer des données et afficher des données.
En appliquant ce design pattern, on obtient le découpage suivant :
- Le container component est en charge de la récupération / traitement des données.
- Le presentational component est en charge de l’affichage des données.
Si on reprend l’exemple précédant en appliquant ce pattern on obtient l’architecture suivante :
- Un container component PokemonListContainer qui appelle l’api PokeAPI et se charge de mettre en forme les données.
- Un presentational component PokemonList qui récupère les données différents des Pokémons et se charge de les afficher. Ici on ajoute un deuxième presentational component PokemonCard qui permet d’afficher chacun des Pokémons dans une card.
Voici l’architecture de dossier que j’ai choisi d’implémenter :
Voici l’implémentation des différents composants.
Container
// PokemonListContainer.jsx
export default function PokemonListContainer() {
const [pokemons, setPokemons] = useState(null);
useEffect(() => {
const fetchPokemons = async () => {
const pokemonsLinks = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
.then((response) => response.json())
.then((response) => response.results);
const pokemons = await Promise.all(
pokemonsLinks.map((pokemon) => fetch(pokemon.url).then((response) => response.json()))
);
setPokemons(pokemons);
};
fetchPokemons().catch(console.error);
}, []);
if (!pokemons) return null;
return <PokemonList pokemons={pokemons} />;
}
Presentational
// PokemonList.jsx
export default function PokemonList({ pokemons }) {
return (
<div className="pokemon-list">
{pokemons.map((pokemon, index) => (
<PokemonCard key={index} pokemon={pokemon} />
))}
</div>
);
}
// PokemonCard.jsx
export default function PokemonCard({ pokemon }) {
return (
<div className="card-container">
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<div className="description-container">
<div className="pokemon-description">
<h1>{pokemon.name}</h1>
<div className="types">
{pokemon.types.map((type, index) => (
<div className="type-chip" key={index}>
{type.type.name}
</div>
))}
</div>
</div>
<div className="stats">
<p>hp : {pokemon.stats[0].base_stat}</p>
<p>attack : {pokemon.stats[1].base_stat}</p>
<p>defense : {pokemon.stats[2].base_stat}</p>
</div>
</div>
</div>
);
}
En appliquant le design pattern Container / Presentational on obtient des fichiers plus concis et plus lisibles. De plus, on sait exactement ce qu’on va trouver dans chacun des fichiers car chacun à une fonction précise.
Avantages
Ce design pattern respect une règle importante de l’architecture logiciel qui est la Separation Of Concern (SOC). Cette règle indique que dans une application, chaque composant logiciel doit avoir une fonction et une seule. Le but est de séparer tous les traitements de nature différente dans des composants séparés (ou modules) et de regrouper ceux qui font les mêmes traitements.
C’est exactement ce qu’on a fait ici. Nous avons le container qui gère la récupération et la transformation des données, tandis que les presentionals gèrent l’affichage des données. On peut voir ces deux modules représentés par les deux dossiers dans l’arborescence de fichier.
Le premier avantage d’appliquer ce design pattern est le gain de lisibilité. Les composants sont beaucoup plus lisibles car ils sont plus petits et tout le code qui est dans ce composant n’a qu’un seul but. On peut anticiper ce qu’on va trouver dans le code et il n’y a pas de mauvaises surprises.
Le deuxième avantage est la facilité de débuggage. En effet, comme la logique est séparée de la vue, il est facile de converger rapidement vers le morceau de code d'où le bug provient. On simplifie le processus de débugge et ainsi on gagne du temps.
Le troisième avantage est la réutilisabilité. En effet, comme la récupération des données, le traitement et l’affichage sont séparés, on obtient des composants qui sont facilement réutilisable. Plus le composant est découpé finement, moins il fait de choses, différentes, plus il sera facile de l’inclure ailleurs dans l’application.
Le dernier avantage est la testabilité. Les presentational components se chargent de créer l’interface utilisateur en fonction des données d’entrées. Il sont donc prévisibles ce qui les rend testable très facilement. On peut faire une analogie entre le presentational component et la fonction pure de la programmation fonctionnelle.
Conclusion
Pour conclure, nous avons vu dans cet article ce qu’est le design pattern container / presentational. Nous avons vu que ce design pattern consiste a séparer la partie récupération / traitement de données de la partie présentation de la vue.
Les avantages d’utiliser ce pattern sont les suivants : la lisibilité, la facilité de débuggage, la réutilisabilité et la testabilité des composants.
Ce design pattern est facilement applicable dans vos différentes applications. Il demande très peu d’effort et apporte beaucoup d’avantages.
Top comments (0)