CodeGym /Cours /Java Server /Référentiel en mémoire pour la liste de lecture

Référentiel en mémoire pour la liste de lecture

Java Server
Niveau 24, Leçon 0
Disponible

1. Rôle du référentiel dans une API en lecture seule

Quand on entend le mot « référentiel » (repository), le cerveau ajoute parfois automatiquement PostgreSQL, les migrations, les index et un DBA qui vous regarde sévèrement à travers l’écran. Mais aujourd’hui nous sommes beaucoup plus modestes : nous avons besoin d’un endroit où il y a des données. Même si l’API est « en lecture seule », elle doit bien lire quelque part, sinon on obtient une API du genre « GET /reading-list renvoie de l’inspiration ».

Si les données sont stockées directement dans le handler (par exemple en y mettant un List<ReadingListItem>), on se retrouve très vite avec le marécage classique du backend : le routage, l’analyse de la requête, la logique métier et le stockage sont mélangés. Et à ce moment-là, vous commencez à écrire un « mini-framework » dans une seule classe, alors que vous vous étiez juré de ne pas le faire.

Dans notre architecture, le référentiel est une couche qui s’occupe d’une chose simple : stocker et fournir des objets du domaine. Il ne connaît rien de HTTP, ne choisit pas les codes de statut, ne sérialise pas le JSON et ne doit pas essayer d’être « plus intelligent qu’il ne le faut ». C’est simplement un « magasinier » : il a un entrepôt (la mémoire du processus) et un fichier (la clé id), et il sait montrer ce qui se trouve sur les étagères.

Limites du stockage en mémoire

Un stockage en mémoire signifie que les données vivent dans la mémoire du processus de l’application Java. C’est-à-dire à l’intérieur de votre JVM lancée, à côté de vos objets, de vos threads et de vos nuits blanches. Vous redémarrez l’application et le stockage est « remis à zéro ». Ce n’est pas un bug, c’est une propriété honnête : nous ne promettons aucune persistance entre les redémarrages aujourd’hui.

Pourquoi est-ce normal dans ce cours ? Parce que notre objectif est d’apprendre la chaîne HTTP et les couches de l’application sans plonger dans un cours voisin sur SQL/JDBC/ORM. Si nous ajoutions une base de données maintenant, 80 % de l’attention irait à la configuration de la connexion, du schéma, des migrations et des subtilités de stockage. Or notre objectif aujourd’hui est plus simple : « faire en sorte que le serveur sache renvoyer une liste et un élément par id », tout en gardant le code compréhensible.

Il est utile de noter les différences dans un petit tableau — non par académisme, mais pour que le cerveau ne se trompe pas :

Question Référentiel en mémoire (Map) Base de données
Les données survivent-elles au redémarrage ? Non Oui
Pilotes/SQL/migrations nécessaires ? Non Oui
Vitesse d’accès par id Très élevée (en mémoire) Dépend des index, du réseau et de la charge
Peut-on stocker des millions d’enregistrements ? Techniquement oui, mais cela devient vite mauvais Oui, c’est justement son rôle
Objectif dans le cours Comprendre les couches et la sémantique HTTP C’est une discipline séparée

Et encore une nuance importante : un référentiel en mémoire est presque toujours une solution « pédagogique ou de prototypage ». Dans un projet réel, il peut être utilisé comme stub temporaire, cache ou mock de test, mais pas comme « solution de stockage finale ». Cependant, pour un cours de transition (bridge course), c’est un compromis idéal : un minimum d’infrastructure, un maximum de focalisation sur l’API.

Contrat du référentiel : parler le langage du domaine, pas HTTP

On a très envie (surtout après une semaine de HTTP) de créer une méthode du genre findByIdOrThrow404 ou findAllAsJson — et apparemment ça « marche ». Mais c’est un piège. Le référentiel ne fait pas partie de la couche web, mais de la logique interne de l’application. Son langage est celui du domaine : ReadingListItem, ReadingStatus, l’identifiant long. Son résultat, ce sont des objets du domaine ou l’absence d’objet du domaine.

Cette séparation est importante non pas pour la « beauté », mais pour maîtriser la complexité. Dès que le référentiel commence à connaître HTTP, vous obtenez une classe qui à la fois « stocke », « décide quel statut renvoyer », « forme le JSON » et « écrit dans les logs ». Une telle classe est impossible à modifier proprement : tout changement impactera toutes les couches à la fois.

Une autre raison de garder le référentiel « sourd à HTTP » est la capacité de remplacement future. Aujourd’hui nous avons Map. Demain (dans la prochaine étape de votre parcours) une base de données peut apparaître. Si le référentiel n’a pas mélangé la logique de stockage avec HTTP, remplacer InMemory... par « Database...Repository » sera beaucoup plus simple : le contrat reste, l’implémentation change.

2. Interface ReadingListRepository

Le niveau d’aujourd’hui est consacré à la partie lecture seule de l’API, donc le référentiel a besoin d’un contrat minimal : « donne tout » et « donne par id ». Pas plus. Quand un débutant voit le mot « Repository », il commence souvent par « et si on ajoutait tout de suite save, update, delete, findByTitle, findByAuthor, findByStatusAndTitleContainsIgnoreCaseAnd… » — et au bout de 15 minutes vous avez un Spring Data JPA maison. Sans Spring Data ni JPA, mais avec toute la douleur.

C’est pourquoi nous fixons une interface simple. Elle vit dans le package readinglist.repository et renvoie le domaine depuis readinglist.domain. C’est un lien important : le référentiel ne renvoie ni DTO, ni chaîne JSON. Il renvoie ReadingListItem, et ensuite une autre couche décidera quoi en faire.

Voici le contrat pour aujourd’hui :

import com.example.readlater.readinglist.domain.ReadingListItem;
import java.util.List;

public interface ReadingListRepository {

    // Retourne tous les objets du domaine tels quels (sans DTO ni JSON).
    List<ReadingListItem> findAll();

    // Retourne l'objet du domaine par son id.
    // Si l'objet n'est pas trouvé, retourne null (et c'est le handler qui décidera si c'est 404).
    ReadingListItem findById(long id);
}

Remarquez le choix petit mais fondamental : findById renvoie ReadingListItem, et si rien n’est trouvé, il renvoie null. On aurait pu utiliser Optional, et dans les grands projets c’est souvent pertinent, mais dans le code pédagogique, null est parfois plus facile à appréhender, surtout quand vous apprenez en parallèle à distinguer 400 et 404 dans le handler. L’essentiel est que null fasse partie du contrat attendu, et non une surprise.

3. InMemoryReadingListRepository avec Map

Passons maintenant à la partie la plus « consistante » de la leçon : comment organiser le stockage. Nous savons que la clé des scénarios de lecture aujourd’hui est id issu du chemin (/reading-list/{id}). Donc la structure la plus naturelle est Map<Long, ReadingListItem>. C’est littéralement un « fichier » : par le numéro de fiche (id) nous trouvons rapidement l’enregistrement correspondant.

La question la plus fréquente ici est « pourquoi pas List ? » Parce que List est en quelque sorte une « étagère sans numéros » : pour trouver un élément par id, vous devriez parcourir tous les enregistrements et comparer item.getId(). Pour quelques éléments ce n’est pas grave, mais le principe n’est pas bon. Map indique immédiatement au cerveau que l’accès principal se fait par clé.

Fixons les champs minimaux de la classe du référentiel. Nous prenons HashMap comme implémentation de base de Map et gardons un compteur id séparément.

import com.example.readlater.readinglist.domain.ReadingListItem;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

public class InMemoryReadingListRepository implements ReadingListRepository {

    // Stockage interne : clé = id, valeur = objet du domaine.
    // Important : on ne doit pas exposer cette Map à l’extérieur, sinon on casse l’encapsulation.
    private final Map<Long, ReadingListItem> storage = new HashMap<>();

    // Générateur d’id uniques au sein du processus (en mémoire).
    // AtomicLong est utile si les requêtes arrivent en parallèle.
    private final AtomicLong nextId = new AtomicLong(0);
}

Ici deux choses sont importantes. Premièrement, storage est une structure interne et nous ne devons pas la divulguer à l’extérieur. Deuxièmement, nextId fait partie de la responsabilité de la couche de stockage. Ce n’est pas le handler qui génère l’id, ni le service, mais bien le référentiel (dans notre simplification pédagogique) qui sait comment délivrer des identifiants uniques au sein du processus.

4. AtomicLong : générateur d’id

Le mot Atomic sonne parfois comme si nous allions nous plonger dans la physique quantique et discuter de « superposition d’identifiants ». En réalité, c’est plus simple : AtomicLong est un compteur qui peut s’incrémenter en toute sécurité (atomiquement), même si plusieurs threads veulent obtenir un nouvel id simultanément.

Oui, aujourd’hui nous ne faisons pas de cours sur la concurrence. Mais un serveur est potentiellement multithread, et même avec le HttpServer pédagogique, les requêtes peuvent arriver en parallèle. Si nous utilisions un simple long et faisions nextId++, on pourrait en théorie obtenir des id identiques en cas de course. AtomicLong nous donne un « numéroteur de tickets » simple et fiable, comme dans une file d’attente électronique.

L’utilisation minimale ressemble à ceci :

// Supposons que nextId est un champ AtomicLong (comme dans le référentiel ci-dessus).
long id1 = nextId.incrementAndGet(); // incrémente le compteur et obtient la nouvelle valeur
long id2 = nextId.incrementAndGet(); // encore une incrémentation

System.out.println(id1); // 1
System.out.println(id2); // 2

La méthode incrementAndGet fait exactement ce qui est écrit sur l’emballage : elle augmente la valeur et retourne la nouvelle. On commence à 0 pour que les premières valeurs soient 1, 2, 3 — c’est plus facile à lire pour les humains que 0, 1, 2 (même si l’ordinateur s’en fiche).

5. Données initiales pour les endpoints GET

Si le référentiel est vide, l’endpoint liste (GET /reading-list) sera quand même correct : il renverra items: [] et count: 0. Mais d’un point de vue pédagogique, c’est ennuyeux : vous ne voyez pas la différence entre « tout fonctionne » et « c’est juste toujours vide ». Il est donc utile d’inclure dans le référentiel quelques exemples de données directement dans le constructeur.

Attention à ne pas confondre : nous ne créons pas un mécanisme complet de création d’éléments via l’API (c’est un autre niveau). Nous préparons simplement un « ensemble de départ » pour pouvoir aujourd’hui tester la lecture de la liste et l’obtention d’un élément par id et voir immédiatement des réponses JSON significatives.

Exemple de constructeur qui place quelques livres :

public InMemoryReadingListRepository() {
    // Les données initiales ne sont nécessaires que pour la démonstration des endpoints en lecture seule.
    putSample("Clean Code", "Robert C. Martin", ReadingStatus.PLANNED, "OL12345M");
    putSample("Effective Java", "Joshua Bloch", ReadingStatus.IN_PROGRESS, "OL67890M");
}

Et voici un petit assistant privé putSample. Il encapsule la logique de génération de l’id et du placement de l’objet dans la Map, afin que le constructeur reste lisible.

private void putSample(String title, String author, ReadingStatus status, String externalId) {
    // Génère un nouvel id unique au sein du processus.
    long id = nextId.incrementAndGet();

    // Crée l’objet du domaine (le référentiel ne crée pas de DTO ni ne forme le JSON).
    ReadingListItem item = new ReadingListItem(id, title, author, status, externalId, null);

    // Place l’enregistrement dans le stockage en mémoire.
    storage.put(id, item);
}

Remarquez le style calme : nous créons un objet du domaine ReadingListItem et le déposons dans storage. Le référentiel ne le sérialise pas, ne le transforme pas en DTO, n’ajoute pas d’« erreurs HTTP ». Il stocke simplement.

6. findAll et findById : implémentation

Passons maintenant aux deux méthodes clés. À première vue, on se dit « il n’y a que deux lignes, qu’y a-t-il à expliquer ? » Eh bien il y a à expliquer, car c’est précisément dans ces endroits que les débutants font souvent une « brèche » dans l’encapsulation et commencent à attraper des bugs étranges qui ressemblent à de la magie.

Commençons par findAll(). La pire chose à faire est de renvoyer une « vue vivante » de la collection interne et de donner au code externe la possibilité de casser accidentellement le référentiel. C’est pourquoi nous retournons une nouvelle liste, construite à partir des valeurs de la Map. C’est peu coûteux, lisible et suffisamment sûr.

import java.util.ArrayList;
import java.util.List;

@Override
public List<ReadingListItem> findAll() {
    // Important : on retourne une copie de la liste pour que le code externe ne puisse pas modifier la composition de la collection.
    return new ArrayList<>(storage.values());
}

Il y a une subtilité : nous copions la liste, mais pas les objets eux-mêmes. Si ReadingListItem est une classe mutable (et elle l’est souvent, car le statut du livre change), quelqu’un à l’extérieur peut modifier l’objet et cela se reflétera dans le référentiel, car la référence est la même. Pour un projet pédagogique, c’est acceptable, mais gardez à l’esprit que « la copie de la liste » protège contre la modification de la composition de la collection, pas contre la modification du contenu des objets.

Passons maintenant à findById(long id). Il doit être le plus simple possible : prendre et retourner depuis la Map par clé. Si rien n’est trouvé, Map.get renverra null. C’est normal. La décision « null → 404 » sera prise dans le handler, car c’est justement une décision HTTP.

@Override
public ReadingListItem findById(long id) {
    // Map.get renvoie null si la clé n’existe pas — c’est le contrat du référentiel.
    return storage.get(id);
}

Et c’est ici qu’on voit bien pourquoi nous ne voulons pas mélanger le référentiel avec HTTP. Le référentiel n’a pas à savoir que null deviendra 404. Pour lui, null signifie simplement : « il n’y a pas ce produit dans l’entrepôt ». Ensuite la couche externe décide s’il s’agit d’une « erreur client », « non trouvé », « conflit » ou autre chose.

7. Le référentiel dans les couches de l’application

Nous venons d’ajouter un nouveau morceau au système, et il est important de ne pas perdre la vue d’ensemble. Quand l’application grandit, le cerveau commence à s’embrouiller : « où est quoi ? pourquoi je fais ça ici et pas là ? » Il est donc utile de fixer une fois le chemin des données de haut en bas et retour.

Imaginez qu’une requête arrive : « donne la liste de lecture ». Le handler (handler) reçoit HTTP, le service (service) résout la tâche applicative « obtenir les données », le référentiel (repository) fournit les objets du domaine, puis tout remonte : le service mappe le domaine vers le DTO de réponse, le handler choisit le statut et écrit le JSON.

Voici un petit schéma qui garde ce modèle en tête :

flowchart TD
    Client[HTTP client / Postman] -->|GET /api/v1/reading-list| Handler[readinglist.http Handler]
    Handler --> Service[readinglist.service ReadingListService]
    Service --> Repo[readinglist.repository ReadingListRepository]
    Repo --> Storage["Map<Long, ReadingListItem>"]
    Storage --> Repo --> Service --> Handler --> Client

L’idée la plus importante ici est la suivante : le référentiel n’est pas la « partie inférieure du handler ». C’est une couche distincte qui peut être comprise, testée et modifiée séparément. Si vous maintenez cette frontière, plus tard (quand de nouvelles opérations apparaîtront) le projet s’étendra de manière prévisible. Si les frontières s’effacent, le projet commencera à s’étendre de façon chaotique, comme un dossier « Nouveau dossier (17) ».

8. Erreurs typiques avec un référentiel en mémoire

Erreur n°1 : stocker des DTO de réponse dans le référentiel au lieu d’objets du domaine.
Cela semble tentant : « pourquoi stocker ReadingListItem si de toute façon on renvoie du JSON ? » Le problème est que le DTO de réponse est le langage du contrat externe, il vit selon les règles du client et peut changer à cause des exigences de l’API. Le référentiel, lui, doit vivre selon les règles du domaine. Si on mélange ces mondes, vous obtenez une couche de stockage qui commence à dépendre de la forme de la réponse HTTP, et tout changement du contrat cassera la partie interne de l’application.

Erreur n°2 : écrire des statuts HTTP et de la logique JSON dans le référentiel.
Un référentiel qui renvoie « 404 » ou construit un ErrorResponse se transforme vite en classe « tout-en-un ». Cela casse le modèle pédagogique des couches : vous cessez de comprendre où s’arrête le stockage et où commence la logique web. Le statut et le JSON doivent naître dans le handler, au maximum avec l’aide d’utilitaires généraux d’envoi de réponse.

Erreur n°3 : générer l’id n’importe où (généralement dans le handler).
Si la génération d’identifiant est dispersée dans le code, vous aurez inévitablement des « collisions inattendues » et des doublons étranges. Même dans un modèle en mémoire, il est préférable de garder la génération en un seul endroit — à côté du stockage. Aujourd’hui nous utilisons AtomicLong pour que les id soient délivrés de manière séquentielle et prévisible, et que le code ne se transforme pas en chasse à celui qui « a incrémenté le compteur en dernier ».

Erreur n°4 : exposer la collection interne ou storage.values() sans copie.
Si vous renvoyez une collection vivante, le code externe peut accidentellement la modifier. Même si vous êtes sûr que « vous ne le ferez pas », quelqu’un d’autre (ou vous-même dans une semaine) le fera. Copier les valeurs dans une nouvelle ArrayList est un moyen peu coûteux de préserver l’encapsulation et de ne pas transformer le référentiel en cuisine collective où chacun peut déplacer les casseroles.

Erreur n°5 : s’attendre à ce que les données en mémoire survivent au redémarrage du serveur.
C’est le piège le plus honnête : vous avez tout configuré, obtenu la liste, vous vous réjouissez, vous redémarrez l’application — et « tout a disparu ». Inutile de chercher un bug. C’est le prix normal du modèle en mémoire. Nous l’avons choisie exprès pour nous concentrer sur HTTP et la structure de l’application. Le stockage persistant est un autre grand sujet, qui viendra plus tard, quand vous aurez des bases solides.

1
Mission
Java Server, niveau 24, leçon 0
Bloqué
Repository en mémoire avec données initiales
Repository en mémoire avec données initiales
1
Mission
Java Server, niveau 24, leçon 0
Bloqué
Copie de la liste au lieu d'une fuite du stockage interne
Copie de la liste au lieu d'une fuite du stockage interne
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION