Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 726 minIntermédiaire

Pattern Repository, DAO et injection de dépendances

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • distinguer un DAO d'un Repository ;
  • faire dépendre un service d'une interface plutôt que d'une classe concrète ;
  • injecter les dépendances par le constructeur, à la main ou avec un conteneur ;
  • tester un service avec de faux dépôts, sans base de données.

4 exercices corrigés1 schéma

Le pattern Repository (comme le DAO, son proche cousin) regroupe dans une classe tout l'accès aux données d'une entité, derrière des méthodes au nom métier (trouverParId, existeChevauchement). L'injection de dépendances consiste à fournir à une classe les objets dont elle a besoin, au lieu de la laisser les créer elle-même. Ensemble, ils permettent de changer de base de données, ou de la remplacer par une fausse dans un test, sans toucher aux règles de gestion. Ce chapitre explique les deux notions et les met en œuvre en C#, en Node.js et en PHP.

DAO et Repository

Les deux patrons isolent le SQL dans des classes dédiées. Ils diffèrent surtout par leur point de vue.

DAO (Data Access Object)Repository
OrigineLes applications Java d'entreprise, au début des années 2000La conception pilotée par le domaine (Domain-Driven Design, Eric Evans, 2003)
Point de vueLa table : une classe par table, des opérations CRUDLe métier : une collection d'objets du domaine, qu'on interroge
Noms des méthodesinsert, update, delete, findById, findAllajouter, trouverParId, existeChevauchement, reservationsAVenir
Ce qu'il renvoieSouvent des lignes ou des objets proches de la tableDes objets du domaine, complets

En pratique, la frontière est floue : beaucoup de classes appelées Repository sont des DAO, et inversement. Au BTS, retiens que les deux isolent l'accès aux données, que le DAO est plus proche de la table et le Repository plus proche du métier. Dans ResaSalles, ReservationRepository.existeChevauchement() est typique d'un Repository : son nom dit une question métier, pas une opération sur une table.

Une interface : le contrat de l'accès aux données

En C#, on décrit ce que le service attend de l'accès aux données par une interface :

csharp
// Data/Interfaces.cs
using ResaSalles.Models;

namespace ResaSalles.Data;

// Le contrat de l'accès aux données : ce dont le service a besoin, sans dire comment c'est fait.
public interface ISalleRepository
{
    List<Salle> Lister();
    Salle? TrouverParId(int id);
}

public interface IReservationRepository
{
    List<Reservation> ListerParSalle(int salleId);
    bool ExisteChevauchement(int salleId, DateTime debut, DateTime fin);
    int Ajouter(Reservation reservation);
}

La classe qui parle à MySQL implémente l'interface :

csharp
// Data/SalleRepository.cs
using MySqlConnector;
using ResaSalles.Models;

namespace ResaSalles.Data;

public class SalleRepository : ISalleRepository
{
    private readonly string _chaineConnexion;

    public SalleRepository(string chaineConnexion)
    {
        _chaineConnexion = chaineConnexion;
    }

    public List<Salle> Lister() => Lire("SELECT id, nom, capacite, visio FROM salle ORDER BY nom", null);

    public Salle? TrouverParId(int id) =>
        Lire("SELECT id, nom, capacite, visio FROM salle WHERE id = @id", id).FirstOrDefault();

    private List<Salle> Lire(string sql, int? id)
    {
        var salles = new List<Salle>();
        using var connexion = new MySqlConnection(_chaineConnexion);
        connexion.Open();
        using var commande = new MySqlCommand(sql, connexion);
        if (id is not null) commande.Parameters.AddWithValue("@id", id);
        using var lecteur = commande.ExecuteReader();
        while (lecteur.Read())
        {
            salles.Add(new Salle
            {
                Id = lecteur.GetInt32("id"),
                Nom = lecteur.GetString("nom"),
                Capacite = lecteur.GetInt32("capacite"),
                Visio = lecteur.GetBoolean("visio"),
            });
        }
        return salles;
    }
}

ReservationRepository implémente IReservationRepository de la même façon, avec les méthodes du ReservationDao du chapitre 5. Le service, lui, ne connaît que les interfaces : son constructeur demande un ISalleRepository et un IReservationRepository, pas un SalleRepository.

C'est le principe d'inversion des dépendances, le D de SOLID : les modules de haut niveau (les règles de gestion) ne doivent pas dépendre des modules de bas niveau (MySQL) ; les deux dépendent d'une abstraction (l'interface). On peut alors brancher n'importe quelle implémentation : MySQL, PostgreSQL, un appel à une API, ou une liste en mémoire pour les tests.

L'injection de dépendances

Une dépendance, c'est un objet dont une classe a besoin pour travailler : le service a besoin d'un Repository et d'une horloge. Il y a deux façons de les obtenir.

Sans injection, la classe crée elle-même ses dépendances :

csharp
public class ReservationService
{
    private readonly ReservationRepository _reservations =
        new ReservationRepository("Server=prod-mysql;Database=resasalles;Uid=app;Pwd=…");
    // …
}

Le service est soudé à MySQL, à une chaîne de connexion précise et à l'horloge réelle. Impossible de le tester sans la vraie base, impossible de lui donner une autre source de données.

Avec injection, la classe déclare ce dont elle a besoin, et quelqu'un d'autre le lui fournit :

csharp
public ReservationService(ISalleRepository salles, IReservationRepository reservations, TimeProvider horloge)
{
    _salles = salles;
    _reservations = reservations;
    _horloge = horloge;
}
Sans injectionReservationServicenewReservationRepositorychaîne de connexion en durSQLMySQLimpossible à tester sans la base,impossible à brancher ailleursAvec injectionRacine de compositionProgram.cs · app.jsinjecteReservationServicedépend de«interface»IReservationRepositoryReservationRepositoryMySQLFausseReservationsliste en mémoireen productiondans les tests
Sans injection, le service est soudé à MySQL ; avec injection, on choisit ce qu'on lui branche

Il existe trois façons d'injecter : par le constructeur (la plus courante, et la seule à retenir pour l'examen : l'objet est complet dès sa création), par une propriété (service.Horloge = …) et par un paramètre de méthode (Reserver(demande, horloge)).

Le référentiel du BTS SIO cite d'ailleurs les « techniques d'injection des dépendances » parmi les savoirs du bloc 2 SLAM, avec la programmation au sein d'un cadre applicatif (framework) : c'est une notion d'examen, pas un raffinement.

La racine de composition

Si personne ne fait new à l'intérieur des classes, il faut bien que quelqu'un crée les objets. Ce quelqu'un, c'est la racine de composition (composition root) : un seul endroit, au démarrage de l'application, qui crée toutes les pièces et les branche entre elles. Dans ResaSalles, c'est Program.cs en C#, app.js en Node.js, public/index.php en PHP.

En Node.js, l'injection se fait à la main, et c'est la pratique la plus répandue :

js
// src/app.js (extrait) : la racine de composition
export function creerApp({ pool, authentifier }) {
  const reservationService = creerReservationService({
    salleRepository: creerSalleRepository(pool),
    reservationRepository: creerReservationRepository(pool),
  });
  const reservationController = creerReservationController(reservationService);

  const app = express();
  app.use(express.json());
  app.use('/api/reservations', authentifier, creerReservationRoutes(reservationController));
  // … gestionnaire d'erreurs
  return app;
}

Même pool et authentifier sont injectés dans creerApp : server.js lui passe le vrai pool MySQL et le vrai middleware d'authentification, un test d'intégration peut lui passer une base de test et un faux utilisateur connecté.

Le conteneur d'injection de .NET

Quand les classes se multiplient, écrire tous les new à la main devient fastidieux. Un conteneur d'injection de dépendances (DI container) le fait pour toi : tu déclares quelle classe fournit quelle interface, et il construit les objets en lisant leurs constructeurs. .NET en fournit un, dans le paquet Microsoft.Extensions.DependencyInjection (déjà inclus dans ASP.NET Core).

csharp
// Program.cs (version avec conteneur)
using Microsoft.Extensions.DependencyInjection;
using ResaSalles.Controllers;
using ResaSalles.Data;
using ResaSalles.Services;
using ResaSalles.Views;

namespace ResaSalles;

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        ApplicationConfiguration.Initialize();

        string chaine = Environment.GetEnvironmentVariable("RESASALLES_DB")
            ?? throw new InvalidOperationException("Variable RESASALLES_DB absente");

        // La racine de composition : on déclare qui fournit quoi, le conteneur assemble.
        var services = new ServiceCollection();
        services.AddSingleton(TimeProvider.System);
        services.AddSingleton<ISalleRepository>(_ => new SalleRepository(chaine));
        services.AddSingleton<IReservationRepository>(_ => new ReservationRepository(chaine));
        services.AddTransient<ReservationService>();
        services.AddTransient<ReservationController>();
        services.AddTransient<FormReservations>();

        using var fournisseur = services.BuildServiceProvider();
        Application.Run(fournisseur.GetRequiredService<FormReservations>());
    }
}

Quand on demande un FormReservations, le conteneur voit que son constructeur demande un ReservationController, qui demande deux Repository et un ReservationService, qui demande à son tour deux Repository et un TimeProvider. Il crée tout dans le bon ordre. Le formulaire reçoit ici seulement son contrôleur : public FormReservations(ReservationController controleur).

La durée de vie dit combien d'instances le conteneur crée :

MéthodeDurée de vieUsage typique
AddSingletonUne seule instance pour toute l'applicationConfiguration, horloge, Repository sans état
AddScopedUne instance par « portée » : par requête HTTP dans ASP.NET CoreContexte de base de données d'Entity Framework, unité de travail
AddTransientUne nouvelle instance à chaque demandeServices légers, contrôleurs, formulaires

Tester un service avec de faux dépôts

C'est le bénéfice le plus concret de l'injection : tester les règles de gestion sans base de données, en quelques millisecondes. En Node.js, avec le lanceur de tests intégré (node --test) :

js
// src/services/reservation.service.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { creerReservationService } from './reservation.service.js';

// De faux dépôts en mémoire : ni serveur, ni base, ni HTTP.
function creerService({ chevauchement = false } = {}) {
  const ajoutees = [];
  const service = creerReservationService({
    salleRepository: { trouverParId: async (id) => (id === 2 ? { id: 2, nom: 'Aiguille', capacite: 4 } : null) },
    reservationRepository: {
      existeChevauchement: async () => chevauchement,
      ajouter: async (r) => (ajoutees.push(r), { id: 1, ...r }),
    },
    maintenant: () => new Date('2026-12-01T08:00:00'),
  });
  return { service, ajoutees };
}

const demande = {
  salleId: 2, employeId: 7, objet: 'Entretien annuel', participants: 2,
  debut: new Date('2026-12-03T14:00:00'), fin: new Date('2026-12-03T15:00:00'),
};

test('une demande valide est enregistrée', async () => {
  const { service, ajoutees } = creerService();
  const r = await service.reserver(demande);
  assert.equal(r.id, 1);
  assert.equal(ajoutees.length, 1);
});

test('plus de participants que de places : refus 422', async () => {
  const { service, ajoutees } = creerService();
  await assert.rejects(service.reserver({ ...demande, participants: 5 }), { statut: 422 });
  assert.equal(ajoutees.length, 0);
});

test('créneau déjà pris : refus 409', async () => {
  const { service } = creerService({ chevauchement: true });
  await assert.rejects(service.reserver(demande), { statut: 409 });
});
text
$ node --test
✔ une demande valide est enregistrée (0.43ms)
✔ plus de participants que de places : refus 422 (0.85ms)
✔ créneau déjà pris : refus 409 (0.08ms)
ℹ tests 3
ℹ pass 3
ℹ fail 0

La sortie est abrégée : Node.js affiche aussi le nombre de tests ignorés et la durée totale. Sans argument, node --test cherche les fichiers de test du projet : ceux dont le nom se termine par .test.js (ou -test.js, _test.js) et ceux rangés dans un dossier test/.

Grâce à l'horloge injectée (maintenant), le test donne le même résultat aujourd'hui et dans dix ans : la date du 3 décembre 2026 est toujours « dans le futur » pour lui. Sans cette injection, dès le 3 décembre 2026 à 14 h, le premier et le troisième tests échoueraient, et le deuxième passerait pour une mauvaise raison (la demande serait refusée parce qu'elle est dans le passé, avec le même statut 422). Un test qui passe n'est pas forcément un test qui vérifie ce que dit son nom.

En C#, on écrit des classes qui implémentent les interfaces :

csharp
class FausseReservations : IReservationRepository
{
    private readonly List<Reservation> _liste = [];
    public List<Reservation> ListerParSalle(int salleId) => _liste.Where(r => r.SalleId == salleId).ToList();
    public bool ExisteChevauchement(int salleId, DateTime debut, DateTime fin) =>
        _liste.Any(r => r.SalleId == salleId && r.Debut < fin && r.Fin > debut);
    public int Ajouter(Reservation reservation) { _liste.Add(reservation); return _liste.Count; }
}

class HorlogeFixe(DateTime maintenant) : TimeProvider
{
    public override DateTimeOffset GetUtcNow() => new DateTimeOffset(maintenant).ToUniversalTime();
}

HorlogeFixe(DateTime maintenant) utilise un constructeur principal (C# 12 et suivants) : le paramètre est accessible dans toute la classe. Avec ces deux classes et une fausse ISalleRepository, on enchaîne six demandes sur la salle Aiguille (4 places), l'horloge étant fixée au 1er décembre 2026 à 8 h :

text
valide : enregistrée (id 1)
trop de monde : refus, La salle Aiguille accueille 4 personnes au plus.
chevauchement : refus, La salle est déjà réservée sur ce créneau.
bout à bout : enregistrée (id 2)
passé : refus, Impossible de réserver dans le passé.
5 h : refus, Une réservation dure 4 heures au plus.

Ces objets de remplacement portent des noms précis : un bouchon (stub) renvoie des réponses fixées d'avance, un faux (fake) a un vrai comportement simplifié (la liste en mémoire), un simulacre (mock) vérifie en plus qu'on l'a appelé comme prévu. Les tests unitaires, les frameworks xUnit et Jest et ces objets de remplacement sont le sujet du cours Tests (mock, stub et spy).

PHP : une interface et un constructeur

En PHP, le même principe s'écrit avec une interface et la promotion de propriété :

php
<?php
declare(strict_types=1);

interface ReservationRepository
{
    public function existeChevauchement(int $salleId, string $debut, string $fin): bool;
    public function ajouter(int $salleId, int $employeId, string $debut, string $fin, string $objet, int $participants): int;
}

final class PdoReservationRepository implements ReservationRepository
{
    public function __construct(private PDO $pdo) {}
    // … les méthodes du ReservationModel du chapitre 3
}

final class ReservationService
{
    public function __construct(
        private SalleRepository $salles,
        private ReservationRepository $reservations,
    ) {}
    // …
}

En PHP, une convention fréquente nomme l'interface d'après le concept (ReservationRepository) et l'implémentation d'après la technique (PdoReservationRepository). En C#, la convention est le préfixe I (IReservationRepository).

Les pièges

  • Une interface pour tout. Créer IReservationService, IReservationController, IFormReservations alors qu'il n'y aura jamais qu'une implémentation n'apporte rien. On crée une interface là où l'on a besoin de remplacer l'implémentation : l'accès aux données, l'horloge, l'envoi d'e-mails, un service externe.
  • La connexion globale. Une classe Database avec une connexion statique, appelée depuis n'importe où (Database::getInstance()->query(...)), est une dépendance cachée : on ne la voit pas dans le constructeur, et on ne peut pas la remplacer. C'est le défaut classique du patron Singleton mal employé.
  • Le localisateur de services. Passer le conteneur lui-même aux classes pour qu'elles s'y servent (fournisseur.GetService<IReservationRepository>() au milieu d'une méthode) cache à nouveau les dépendances. Seule la racine de composition parle au conteneur.
  • Trop de dépendances. Un constructeur qui demande huit objets signale une classe qui fait trop de choses : découpe-la.

Exercices

Exercice 1 sur 4 Repérer les dépendances cachées Réussi À revoir

Dans cette classe C#, relève les dépendances que la classe crée elle-même, puis réécris le constructeur pour les injecter.

csharp
public class RappelService
{
    public void EnvoyerRappels()
    {
        var repo = new ReservationRepository("Server=localhost;Database=resasalles;Uid=app;Pwd=x");
        var smtp = new SmtpMailer("smtp.brevent.example", 587);
        foreach (var r in repo.ReservationsDeDemain(DateTime.Today))
            smtp.Envoyer(r.EmailEmploye, "Rappel", $"Demain : {r.Objet}");
    }
}
Voir la réponseMasquer la réponse de l'exercice 1

Trois dépendances cachées : le Repository (et sa chaîne de connexion en dur), le service d'envoi d'e-mails, et l'horloge (DateTime.Today).

csharp
public interface IMailer { void Envoyer(string destinataire, string sujet, string corps); }

public class RappelService
{
    private readonly IReservationRepository _reservations;
    private readonly IMailer _mailer;
    private readonly TimeProvider _horloge;

    public RappelService(IReservationRepository reservations, IMailer mailer, TimeProvider horloge)
    {
        _reservations = reservations;
        _mailer = mailer;
        _horloge = horloge;
    }

    public void EnvoyerRappels()
    {
        DateTime aujourdhui = _horloge.GetLocalNow().Date;
        foreach (var r in _reservations.ReservationsDeDemain(aujourdhui))
            _mailer.Envoyer(r.EmailEmploye, "Rappel", $"Demain : {r.Objet}");
    }
}

La méthode ReservationsDeDemain, qui renvoie des objets portant l'objet de la réunion et l'e-mail de l'employé, doit figurer dans l'interface IReservationRepository. Un test peut maintenant passer un faux IMailer qui enregistre les e-mails au lieu de les envoyer, et vérifier qu'il y en a autant que de réservations du lendemain.

Tu avais trouvé ?

Exercice 2 sur 4 DAO ou Repository ? Réussi À revoir

Classe chaque ensemble de méthodes : plutôt DAO ou plutôt Repository ?

  1. insert(Salle), update(Salle), delete(int), findById(int), findAll().
  2. sallesLibres(DateTime debut, DateTime fin, int participants), ajouter(Salle), trouverParNom(string).
  3. executerRequete(string sql).
Voir la réponseMasquer la réponse de l'exercice 2
  1. DAO : les opérations CRUD d'une table, nommées d'après la technique.
  2. Repository : sallesLibres exprime une question métier (« quelles salles sont libres sur ce créneau pour ce nombre de personnes ? »).
  3. Ni l'un ni l'autre : c'est une simple enveloppe autour du pilote, qui laisse le SQL se répandre dans les couches supérieures. À éviter.

Tu avais trouvé ?

Exercice 3 sur 4 Choisir la durée de vie Réussi À revoir

Pour une API ASP.NET Core, choisis AddSingleton, AddScoped ou AddTransient pour : la configuration de l'application ; le contexte Entity Framework d'accès à la base ; un service de calcul de tarif sans état ; un cache en mémoire des salles partagé par toutes les requêtes.

Voir la réponseMasquer la réponse de l'exercice 3
  • Configuration : AddSingleton, elle ne change pas pendant l'exécution.
  • Contexte Entity Framework : AddScoped, une instance par requête HTTP, qui suit les modifications de la requête et n'est pas partagée entre requêtes simultanées (il n'est pas prévu pour être utilisé par plusieurs fils d'exécution).
  • Service de calcul sans état : AddTransient (ou AddSingleton, sans risque puisqu'il n'a pas d'état).
  • Cache partagé : AddSingleton, à condition qu'il soit sûr en accès concurrent (ConcurrentDictionary par exemple). Attention : un singleton ne doit pas dépendre d'un service Scoped.

Tu avais trouvé ?

Exercice 4 sur 4 Écrire un test avec un faux Réussi À revoir

En t'inspirant du test Node.js du chapitre, écris un test qui vérifie qu'une réservation de 9 h à 14 h est refusée avec le statut 422, et que rien n'est ajouté.

Voir la réponseMasquer la réponse de l'exercice 4
js
test('plus de 4 heures : refus 422', async () => {
  const { service, ajoutees } = creerService();
  await assert.rejects(
    service.reserver({ ...demande, debut: new Date('2026-12-03T09:00:00'), fin: new Date('2026-12-03T14:00:00') }),
    { statut: 422, message: 'Une réservation dure 4 heures au plus' },
  );
  assert.equal(ajoutees.length, 0);
});

9 h à 14 h, c'est 300 minutes, plus que les 240 permises. assert.rejects vérifie que la promesse est rejetée et que l'erreur a les propriétés indiquées. La deuxième assertion prouve que le service n'a pas appelé ajouter : la règle est vérifiée avant l'écriture.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Architecture hexagonale et clean architecture : aperçuRevoir le sommaire du cours