Aller au contenu principal

🔂 Singleton et invertion des dépendances

🎯 Objectifs pédagogiques​

  • Mettre en pratique le patron de conception Singleton.
  • Comprendre l'inversion des dĂ©pendances et son utilitĂ©.

📜 Énoncé​

Le code ci-dessous implémente un service de configuration qui doit être unique dans l'application. Il n'utilise pas le patron singleton ni l'inversion des dépendances. Vous devez réécrire le code et l'adapter pour respecter ces principes selon les intructions détaillées plus bas.

import json
import os

# Code problématique à refactoriser
class ConfigurationService:
def __init__(self, config_file="config.json"):
self.config_file = os.path.join(os.path.dirname(__file__), config_file)
self.config = {}
self.load_config()

def load_config(self):
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
self.config = json.load(f)
else:
# Configuration par défaut
self.config = {
"database_url": "localhost:5432",
"api_key": "default_key",
"debug_mode": True,
"max_connections": 10
}

def get_config(self, key):
return self.config.get(key)

def set_config(self, key, value):
self.config[key] = value

def save_config(self):
with open(self.config_file, 'w') as f:
json.dump(self.config, f, indent=2)

class DatabaseConnection:
def __init__(self):
# Problème: création directe d'une dépendance
self.config_service = ConfigurationService()
self.connection_string = self.config_service.get_config("database_url")
self.max_connections = self.config_service.get_config("max_connections")

def connect(self):
print(f"Connexion à la base de données: {self.connection_string}")
print(f"Nombre max de connexions: {self.max_connections}")

class ApiClient:
def __init__(self):
# Problème: création directe d'une autre instance
self.config_service = ConfigurationService()
self.api_key = self.config_service.get_config("api_key")
self.debug_mode = self.config_service.get_config("debug_mode")

def make_request(self, endpoint):
debug_info = f" [DEBUG MODE]" if self.debug_mode else ""
print(f"Requête API vers {endpoint} avec clé: {self.api_key}{debug_info}")

class Logger:
def __init__(self):
# Problème: encore une autre instance
self.config_service = ConfigurationService()
self.debug_mode = self.config_service.get_config("debug_mode")

def log(self, message):
if self.debug_mode:
print(f"[LOG] {message}")

# Code d'utilisation problématique
def main():
# Chaque classe crée sa propre instance de ConfigurationService
db = DatabaseConnection()
api = ApiClient()
logger = Logger()

db.connect()
api.make_request("/users")
logger.log("Application démarrée")

# Si on modifie la configuration dans une instance...
db.config_service.set_config("debug_mode", False)
db.config_service.save_config()

# Les autres instances ne voient pas le changement!
logger.log("Cette ligne devrait-elle s'afficher?")

if __name__ == "__main__":
main()

🔍 Analyse des problèmes​

Le code ci-dessus présente plusieurs problèmes majeurs :

1. Absence du patron Singleton​

  • Chaque classe crĂ©e sa propre instance de ConfigurationService
  • Cela peut mener Ă  des incohĂ©rences dans la configuration
  • Les modifications ne sont pas partagĂ©es entre les instances

2. Violation du principe d'inversion des dépendances​

  • Les classes DatabaseConnection, ApiClient et Logger crĂ©ent directement leurs dĂ©pendances
  • Elles sont fortement couplĂ©es Ă  ConfigurationService
  • Difficile de tester unitairement ou de changer l'implĂ©mentation

3. Problèmes de synchronisation​

  • Si une partie de l'application modifie la configuration, les autres parties ne le voient pas
  • Risque d'Ă©tats incohĂ©rents dans l'application

📋 Instructions détaillées​

Partie 1: Implémentation du patron Singleton​

Transformez la classe ConfigurationService en Singleton en utilisant la méthode spéciale __new__ pour contrôler la création de l'instance.

Partie 2: Application de l'inversion des dépendances​

Refactorisez les classes DatabaseConnection, ApiClient et Logger pour qu'elles respectent le principe d'inversion des dépendances.

Partie 2.1 Création d'une interface abstraite​

Créez une classe abstraite ConfigInterface qui définit le contrat :

from abc import ABC, abstractmethod

class ConfigInterface(ABC):
@abstractmethod
def get_config(self, key: str):
pass

@abstractmethod
def set_config(self, key: str, value):
pass

Partie 2.2 Injection de dépendances​

Modifiez les classes pour qu'elles reçoivent leur dépendance via le constructeur.

Solution​

Le fichier singleton_di.py contient la solution complète.