Dependency Inversion : découpler votre code de la base de données


Le piège : des services soudés à la base de données

Voici un pattern qu’on retrouve dans beaucoup de codebases :

public class OrderService
{
    public Order GetOrder(int id)
    {
        var conn = new OracleConnection(
            "Data Source=PROD_DB;User Id=admin;Password=...");
        var cmd = new OracleCommand(
            "SELECT * FROM Orders WHERE Id = @id", conn);
        cmd.Parameters.AddWithValue("@id", id);

        conn.Open();
        var reader = cmd.ExecuteReader();
        // ... mapping manuel des colonnes
    }
}

Le service métier instancie directement une connexion Oracle, écrit du SQL brut, et mappe les résultats à la main.

Problème ? Ce pattern se retrouve dans chaque service :

graph TD
    A["🛒 OrderService"] -->|SQL direct| DB[("🔥 Oracle Prod 🔥")]
    B["👤 UserService"] -->|SQL direct| DB
    C["📧 NotificationService"] -->|SQL direct| DB
    D["💳 PaymentService"] -->|SQL direct| DB

    style DB fill:#8B0000,stroke:#FFD700,color:#fff,stroke-width:3px
    style A fill:#1e293b,stroke:#94a3b8,color:#fff
    style B fill:#1e293b,stroke:#94a3b8,color:#fff
    style C fill:#1e293b,stroke:#94a3b8,color:#fff
    style D fill:#1e293b,stroke:#94a3b8,color:#fff

Chaque service connaît Oracle. Chaque service sait construire une connexion, écrire du SQL, et parser des résultats. La base de données est partout dans le code.

Les conséquences concrètes

Ce couplage a des effets en cascade qui paralysent progressivement l’équipe :

Impossible de tester sans la base de prod

// Pour tester OrderService, il faut :
// 1. Une instance Oracle qui tourne
// 2. Des données cohérentes dans toutes les tables
// 3. Un réseau qui atteint la base
// Résultat : les tests ne tournent qu'en CI, et encore...
[TestMethod]
public void GetOrder_ReturnsOrder()
{
    var service = new OrderService(); // 💥 besoin d'Oracle
    var order = service.GetOrder(42); // 💥 besoin de données en base
    Assert.IsNotNull(order);
}

Impossible de changer de base de données

Le jour où l’entreprise veut migrer d’Oracle vers PostgreSQL, chaque service doit être réécrit. Les requêtes SQL sont différentes, les types sont différents, les connexions sont différentes.

Impossible de refactorer sans risquer la prod

Toucher un service, c’est toucher du SQL qui tape sur la base de production. Un oubli dans une clause WHERE, et c’est l’incident.

graph LR
    Dev["🧑‍💻 Développeur"] -->|"modifie"| Code["OrderService"]
    Code -->|"SQL direct"| Prod[("🔥 Base de Prod")]

    style Prod fill:#8B0000,stroke:#FFD700,color:#fff,stroke-width:3px
    style Dev fill:#1e293b,stroke:#94a3b8,color:#fff
    style Code fill:#1e293b,stroke:#94a3b8,color:#fff

Pas de filet de sécurité. Pas d’abstraction entre le code et la production.

Le Dependency Inversion Principle

Robert C. Martin l’a formulé ainsi :

“Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.”

Concrètement : OrderService (haut niveau, logique métier) ne devrait pas dépendre d’OracleConnection (bas niveau, infrastructure). Les deux devraient dépendre d’une interface.

Avant / Après : l’inversion en action

Avant : couplage direct

// OrderService DÉPEND de OracleConnection
public class OrderService
{
    public Order GetOrder(int id)
    {
        var conn = new OracleConnection("Data Source=PROD_DB;...");
        var cmd = new OracleCommand(
            "SELECT * FROM Orders WHERE Id = @id", conn);
        // ... Oracle partout
    }

    public void PlaceOrder(Order order)
    {
        var conn = new OracleConnection("Data Source=PROD_DB;...");
        var cmd = new OracleCommand(
            "INSERT INTO Orders ...", conn);
        // ... encore Oracle
    }
}

Le sens de la dépendance va du métier vers l’infrastructure :

graph LR
    OS["OrderService<br/>(métier)"] -->|"dépend de"| OC["OracleConnection<br/>(infrastructure)"]

    style OS fill:#1e40af,stroke:#93c5fd,color:#fff
    style OC fill:#8B0000,stroke:#FFD700,color:#fff

Après : dépendance inversée

On introduit une interface que le service métier définit selon ses besoins :

// L'interface est définie par le code métier
public interface IOrderRepository
{
    Order GetById(int id);
    void Save(Order order);
    IReadOnlyList<Order> GetByCustomer(int customerId);
}

Le service métier ne dépend que de cette interface :

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
        => _repository = repository;

    public Order GetOrder(int id)
        => _repository.GetById(id);

    public void PlaceOrder(Order order)
    {
        order.Validate();
        _repository.Save(order);
    }
}

L’implémentation Oracle est isolée dans une classe d’infrastructure :

public class OracleOrderRepository : IOrderRepository
{
    private readonly string _connectionString;

    public OracleOrderRepository(string connectionString)
        => _connectionString = connectionString;

    public Order GetById(int id)
    {
        using var conn = new OracleConnection(_connectionString);
        // SQL Oracle ici, et seulement ici
    }

    public void Save(Order order) { /* ... */ }
    public IReadOnlyList<Order> GetByCustomer(int customerId) { /* ... */ }
}

Le sens de la dépendance est inversé :

graph TD
    OS["OrderService<br/>(métier)"] -->|"dépend de"| I["IOrderRepository<br/>(abstraction)"]
    ORA["OracleOrderRepository<br/>(infrastructure)"] -.->|"implémente"| I

    style OS fill:#1e40af,stroke:#93c5fd,color:#fff
    style I fill:#15803d,stroke:#86efac,color:#fff
    style ORA fill:#78350f,stroke:#fbbf24,color:#fff

OrderService ne sait plus qu’Oracle existe. Il parle à une interface. L’implémentation Oracle est un détail d’infrastructure, interchangeable.

L’architecture complète

Avec le Dependency Inversion appliqué à tous les services, l’architecture ressemble à ceci :

graph TD
    A["🛒 OrderService"] -->|"dépend de"| IA["IOrderRepository"]
    B["👤 UserService"] -->|"dépend de"| IB["IUserRepository"]
    C["💳 PaymentService"] -->|"dépend de"| IC["IPaymentRepository"]

    ORA_A["OracleOrderRepo"] -.->|"implémente"| IA
    ORA_B["OracleUserRepo"] -.->|"implémente"| IB
    ORA_C["OraclePaymentRepo"] -.->|"implémente"| IC

    MOCK_A["InMemoryOrderRepo"] -.->|"implémente"| IA
    MOCK_B["InMemoryUserRepo"] -.->|"implémente"| IB
    MOCK_C["InMemoryPaymentRepo"] -.->|"implémente"| IC

    ORA_A --> DB[("Oracle Prod")]
    ORA_B --> DB
    ORA_C --> DB

    style IA fill:#15803d,stroke:#86efac,color:#fff
    style IB fill:#15803d,stroke:#86efac,color:#fff
    style IC fill:#15803d,stroke:#86efac,color:#fff
    style DB fill:#78350f,stroke:#fbbf24,color:#fff

Les interfaces (en vert) sont le bouclier. Elles protègent le code métier du monde extérieur.

Ce que ça débloque

Des tests sans base de données

[TestMethod]
public void PlaceOrder_SavesOrderToRepository()
{
    // Arrange - implémentation en mémoire, pas besoin d'Oracle
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);
    var order = new Order { CustomerId = 1, Total = 99.99m };

    // Act
    service.PlaceOrder(order);

    // Assert
    var saved = repository.GetById(order.Id);
    Assert.IsNotNull(saved);
    Assert.AreEqual(99.99m, saved.Total);
}

Les tests tournent en millisecondes, sans infrastructure, sur n’importe quelle machine.

Une migration de base de données indolore

Le jour où il faut migrer vers PostgreSQL :

public class PostgresOrderRepository : IOrderRepository
{
    public Order GetById(int id) { /* SQL PostgreSQL */ }
    public void Save(Order order) { /* ... */ }
    public IReadOnlyList<Order> GetByCustomer(int customerId) { /* ... */ }
}

Aucun service métier n’est modifié. Il faut bien sûr écrire les nouveaux repositories PostgreSQL, mais la configuration de l’injection de dépendances, elle, ne change qu’en une ligne :

// Avant
services.AddScoped<IOrderRepository, OracleOrderRepository>();

// Après
services.AddScoped<IOrderRepository, PostgresOrderRepository>();

Le passage de l’un à l’autre se fait en une ligne (et en implémentant les nouveaux repositories, bien sûr). Le code métier, lui, ne sait même pas que la base a changé.

Note : si vos repositories utilisent un ORM comme Entity Framework ou Hibernate, la migration est quasi littéralement une ligne — l’ORM abstrait déjà les différences entre bases de données.

Le flux de décision

Voici comment l’architecture guide les choix de l’équipe :

flowchart TD
    Start["Nouveau besoin d'accès aux données ?"] --> Q1{"Le service métier a-t-il<br/>besoin d'une nouvelle opération ?"}
    Q1 -->|Oui| AddMethod["Ajouter la méthode<br/>à l'interface"]
    Q1 -->|Non| UseExisting["Utiliser les méthodes existantes"]
    AddMethod --> Implement["Implémenter dans<br/>OracleRepository"]
    AddMethod --> MockImpl["Implémenter dans<br/>InMemoryRepository"]
    Implement --> Test["Tester le service métier<br/>avec InMemory"]
    MockImpl --> Test
    UseExisting --> Test
    Test --> Deploy["Déployer en confiance"]

    style Start fill:#1e293b,stroke:#94a3b8,color:#fff
    style AddMethod fill:#15803d,stroke:#86efac,color:#fff
    style Test fill:#1e40af,stroke:#93c5fd,color:#fff
    style Deploy fill:#15803d,stroke:#86efac,color:#fff

L’erreur courante : l’interface qui copie la base

Attention à un piège fréquent. Certains créent des interfaces qui calquent la structure de la base de données :

// ❌ L'interface expose les détails de la base
public interface IOrderRepository
{
    DataTable ExecuteQuery(string sql);
    int ExecuteNonQuery(string sql);
    DbDataReader GetReader(string tableName);
}

Ce n’est pas du Dependency Inversion. C’est juste une indirection. L’interface doit être définie selon les besoins du code métier, pas selon la technologie sous-jacente :

// ✅ L'interface exprime les besoins métier
public interface IOrderRepository
{
    Order GetById(int id);
    void Save(Order order);
    IReadOnlyList<Order> GetByCustomer(int customerId);
    IReadOnlyList<Order> GetPendingOrders();
}

La différence ? La première interface oblige le code métier à écrire du SQL. La seconde le libère de toute connaissance technique.

En résumé

  • Le couplage direct à la base de données empêche les tests, le refactoring et la migration
  • Le Dependency Inversion Principle place une abstraction entre le code métier et l’infrastructure
  • L’interface est définie par le code métier, pas par la base de données
  • Les implémentations (Oracle, PostgreSQL, InMemory) sont interchangeables
  • Les tests deviennent rapides, fiables et indépendants de l’infrastructure

Le principe est simple : votre code métier ne devrait jamais savoir quelle base de données il utilise. Si changer de base se résume à implémenter de nouveaux repositories et modifier une ligne de configuration, vous avez gagné.


Cet article est tiré de ma conférence Le Seigneur du Legacy, où le Balrog représente ce couplage profond à la base de production — un danger qui sommeille dans les fondations du code depuis des années. Disponible en Brown Bag Lunch dans vos locaux.