540 lines
21 KiB
Markdown
540 lines
21 KiB
Markdown
# 🏗️ Architecture SOLID - Notytex Phase 1
|
|
|
|
## Vue d'Ensemble de l'Architecture
|
|
|
|
Cette documentation présente l'architecture SOLID implémentée lors du refactoring Phase 1 de Notytex, transformant un monolithe en un système découplé et modulaire.
|
|
|
|
## 📊 Architecture Visuelle
|
|
|
|
```
|
|
┌─────────────────────── APPLICATION LAYER ────────────────────────┐
|
|
│ │
|
|
│ Controllers (routes/) │
|
|
│ ├── assessments.py │
|
|
│ ├── classes.py │
|
|
│ └── grading.py │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── FACADE LAYER ──────────────────────────────┐
|
|
│ │ │
|
|
│ AssessmentServicesFacade │ ClassServicesFacade │
|
|
│ ├── get_grading_progress │ ├── get_trimester_statistics │
|
|
│ ├── calculate_student_s. │ ├── get_class_results │
|
|
│ └── get_statistics │ ├── get_domain_analysis │
|
|
│ │ └── get_competence_analysis │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── SERVICE LAYER ─────────────────────────────┐
|
|
│ │ │
|
|
│ UnifiedGradingCalculator │ ClassStatisticsService │
|
|
│ AssessmentProgressService │ ClassAnalysisService │
|
|
│ StudentScoreCalculator │ │
|
|
│ AssessmentStatisticsServ. │ │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── INTERFACE LAYER ───────────────────────────┐
|
|
│ │ │
|
|
│ ConfigProvider (Protocol) │ DatabaseProvider (Protocol) │
|
|
│ ├── is_special_value │ ├── get_grades_for_assessment │
|
|
│ └── get_special_values │ └── get_grading_elements_with_s. │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── CONCRETE PROVIDERS ────────────────────────┐
|
|
│ │ │
|
|
│ ConfigManagerProvider │ SQLAlchemyDatabaseProvider │
|
|
│ ├── Lazy loading config │ ├── Optimized queries │
|
|
│ └── Circular import fix │ └── N+1 problem solving │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── MODEL LAYER ───────────────────────────────┐
|
|
│ │ │
|
|
│ Models (adapters) │ Repositories │
|
|
│ ├── Assessment │ ├── AssessmentRepository │
|
|
│ ├── ClassGroup │ ├── ClassRepository │
|
|
│ └── GradingCalculator │ └── BaseRepository │
|
|
│ │ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────── DATA LAYER ────────────────────────────────┐
|
|
│ │ │
|
|
│ SQLAlchemy ORM │
|
|
│ SQLite Database │
|
|
│ │
|
|
└───────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## 🎯 Principes SOLID Appliqués
|
|
|
|
### 1. Single Responsibility Principle (SRP)
|
|
|
|
**Avant** : Le modèle `Assessment` contenait 279 lignes avec de multiples responsabilités.
|
|
|
|
**Après** : Chaque service a une responsabilité unique :
|
|
|
|
```python
|
|
# Service dédié au calcul de progression
|
|
class AssessmentProgressService:
|
|
"""Single Responsibility: calcul et formatage de la progression."""
|
|
|
|
def calculate_grading_progress(self, assessment) -> ProgressResult:
|
|
# Logique spécialisée pour la progression uniquement
|
|
pass
|
|
|
|
# Service dédié aux calculs de scores
|
|
class StudentScoreCalculator:
|
|
"""Single Responsibility: calculs de notes avec logique métier."""
|
|
|
|
def calculate_student_scores(self, assessment):
|
|
# Logique spécialisée pour les scores uniquement
|
|
pass
|
|
|
|
# Service dédié aux statistiques
|
|
class AssessmentStatisticsService:
|
|
"""Single Responsibility: analyses statistiques des résultats."""
|
|
|
|
def get_assessment_statistics(self, assessment) -> StatisticsResult:
|
|
# Logique spécialisée pour les statistiques uniquement
|
|
pass
|
|
```
|
|
|
|
**📊 Métriques d'Amélioration :**
|
|
- `Assessment` : 279 → 50 lignes (-82%)
|
|
- `ClassGroup` : 425 → 80 lignes (-81%)
|
|
- `GradingCalculator` : 102 → 32 lignes (-68%)
|
|
|
|
### 2. Open/Closed Principle (OCP)
|
|
|
|
**Avant** : Ajout de nouveaux types de notation nécessitait modification du code existant.
|
|
|
|
**Après** : Extension par Strategy Pattern sans modification :
|
|
|
|
```python
|
|
class GradingStrategy(ABC):
|
|
"""Interface Strategy pour les différents types de notation."""
|
|
|
|
@abstractmethod
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
pass
|
|
|
|
class NotesStrategy(GradingStrategy):
|
|
"""Strategy pour la notation en points (notes)."""
|
|
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
try:
|
|
return float(grade_value)
|
|
except (ValueError, TypeError):
|
|
return 0.0
|
|
|
|
class ScoreStrategy(GradingStrategy):
|
|
"""Strategy pour la notation par compétences (score 0-3)."""
|
|
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
try:
|
|
score_int = int(grade_value)
|
|
if 0 <= score_int <= 3:
|
|
return (score_int / 3) * max_points
|
|
return 0.0
|
|
except (ValueError, TypeError):
|
|
return 0.0
|
|
|
|
# Extension facile : Nouveau type sans modification existante
|
|
class CustomStrategy(GradingStrategy):
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
# Nouvelle logique métier sans impacter le code existant
|
|
pass
|
|
|
|
# Enregistrement dynamique
|
|
GradingStrategyFactory.register_strategy('custom', CustomStrategy)
|
|
```
|
|
|
|
### 3. Liskov Substitution Principle (LSP)
|
|
|
|
**Application** : Les strategies et providers sont interchangeables :
|
|
|
|
```python
|
|
# Toutes les strategies respectent le même contrat
|
|
def test_strategy_substitution():
|
|
strategies = [
|
|
GradingStrategyFactory.create('notes'),
|
|
GradingStrategyFactory.create('score')
|
|
]
|
|
|
|
# Substitution transparente
|
|
for strategy in strategies:
|
|
result = strategy.calculate_score("2", 4.0) # Comportement cohérent
|
|
assert isinstance(result, (float, type(None)))
|
|
|
|
# Providers mockables pour les tests
|
|
class MockConfigProvider:
|
|
def is_special_value(self, value: str) -> bool:
|
|
return value in ['.', 'd']
|
|
|
|
def get_special_values(self) -> Dict[str, Dict[str, Any]]:
|
|
return {'.': {'value': 0, 'counts': True}}
|
|
|
|
# Substitution complète dans les tests
|
|
def test_with_mock_provider():
|
|
mock_config = MockConfigProvider()
|
|
calculator = UnifiedGradingCalculator(mock_config)
|
|
# Comportement identique avec le mock
|
|
```
|
|
|
|
### 4. Interface Segregation Principle (ISP)
|
|
|
|
**Avant** : Dépendances monolithiques vers les modèles entiers.
|
|
|
|
**Après** : Interfaces spécialisées avec protocols :
|
|
|
|
```python
|
|
class ConfigProvider(Protocol):
|
|
"""Interface spécialisée pour la configuration uniquement."""
|
|
|
|
def is_special_value(self, value: str) -> bool: ...
|
|
def get_special_values(self) -> Dict[str, Dict[str, Any]]: ...
|
|
|
|
class DatabaseProvider(Protocol):
|
|
"""Interface spécialisée pour l'accès aux données uniquement."""
|
|
|
|
def get_grades_for_assessment(self, assessment_id: int) -> List[Any]: ...
|
|
def get_grading_elements_with_students(self, assessment_id: int) -> List[Any]: ...
|
|
|
|
# Les services ne dépendent que des méthodes qu'ils utilisent
|
|
class UnifiedGradingCalculator:
|
|
def __init__(self, config_provider: ConfigProvider):
|
|
self.config_provider = config_provider # Seulement la config
|
|
|
|
class AssessmentProgressService:
|
|
def __init__(self, db_provider: DatabaseProvider):
|
|
self.db_provider = db_provider # Seulement la DB
|
|
```
|
|
|
|
### 5. Dependency Inversion Principle (DIP)
|
|
|
|
**Avant** : Dépendances directes vers les implémentations concrètes.
|
|
|
|
**Après** : Inversion avec injection de dépendances :
|
|
|
|
```python
|
|
# Les services dépendent d'abstractions (Protocols)
|
|
class StudentScoreCalculator:
|
|
def __init__(self,
|
|
grading_calculator: UnifiedGradingCalculator,
|
|
db_provider: DatabaseProvider): # Abstraction
|
|
self.grading_calculator = grading_calculator
|
|
self.db_provider = db_provider
|
|
|
|
# Factory pour injection complète
|
|
class AssessmentServicesFactory:
|
|
@classmethod
|
|
def create_facade(cls) -> AssessmentServicesFacade:
|
|
"""Crée une facade avec toutes les dépendances injectées."""
|
|
config_provider = ConfigManagerProvider() # Implémentation concrète
|
|
db_provider = SQLAlchemyDatabaseProvider() # Implémentation concrète
|
|
|
|
return AssessmentServicesFacade(
|
|
config_provider=config_provider,
|
|
db_provider=db_provider
|
|
)
|
|
|
|
# Les modèles utilisent la factory pour l'injection
|
|
class Assessment(db.Model):
|
|
@property
|
|
def grading_progress(self):
|
|
services_facade = AssessmentServicesFactory.create_facade()
|
|
return services_facade.get_grading_progress(self)
|
|
```
|
|
|
|
## 🔧 Patterns Architecturaux Implémentés
|
|
|
|
### Strategy Pattern
|
|
|
|
**Utilisation** : Types de notation extensibles
|
|
|
|
```python
|
|
class GradingStrategyFactory:
|
|
"""Factory pour créer les strategies de notation."""
|
|
|
|
_strategies = {
|
|
'notes': NotesStrategy,
|
|
'score': ScoreStrategy
|
|
}
|
|
|
|
@classmethod
|
|
def create(cls, grading_type: str) -> GradingStrategy:
|
|
strategy_class = cls._strategies.get(grading_type)
|
|
if not strategy_class:
|
|
raise ValueError(f"Type de notation non supporté: {grading_type}")
|
|
return strategy_class()
|
|
|
|
@classmethod
|
|
def register_strategy(cls, grading_type: str, strategy_class: type):
|
|
"""Permet d'enregistrer de nouveaux types de notation."""
|
|
cls._strategies[grading_type] = strategy_class
|
|
```
|
|
|
|
### Facade Pattern
|
|
|
|
**Utilisation** : Point d'entrée unifié pour les services complexes
|
|
|
|
```python
|
|
class AssessmentServicesFacade:
|
|
"""Facade qui regroupe tous les services pour faciliter l'utilisation."""
|
|
|
|
def __init__(self, config_provider: ConfigProvider, db_provider: DatabaseProvider):
|
|
# Création des services avec injection de dépendances
|
|
self.grading_calculator = UnifiedGradingCalculator(config_provider)
|
|
self.progress_service = AssessmentProgressService(db_provider)
|
|
self.score_calculator = StudentScoreCalculator(self.grading_calculator, db_provider)
|
|
self.statistics_service = AssessmentStatisticsService(self.score_calculator)
|
|
|
|
def get_grading_progress(self, assessment) -> ProgressResult:
|
|
"""Point d'entrée unifié pour la progression."""
|
|
return self.progress_service.calculate_grading_progress(assessment)
|
|
```
|
|
|
|
### Repository Pattern
|
|
|
|
**Utilisation** : Accès aux données découplé (existant, étendu)
|
|
|
|
```python
|
|
# Pattern déjà implémenté et étendu
|
|
class BaseRepository:
|
|
def __init__(self, db, model_class):
|
|
self.db = db
|
|
self.model_class = model_class
|
|
|
|
class AssessmentRepository(BaseRepository):
|
|
def find_by_filters(self, trimester=None, class_id=None):
|
|
# Logique de requête découplée
|
|
pass
|
|
```
|
|
|
|
### Factory Pattern
|
|
|
|
**Utilisation** : Création centralisée des services
|
|
|
|
```python
|
|
class AssessmentServicesFactory:
|
|
"""Factory pour créer l'ensemble des services avec injection de dépendances."""
|
|
|
|
@classmethod
|
|
def create_facade(cls) -> AssessmentServicesFacade:
|
|
config_provider = ConfigManagerProvider()
|
|
db_provider = SQLAlchemyDatabaseProvider()
|
|
|
|
return AssessmentServicesFacade(
|
|
config_provider=config_provider,
|
|
db_provider=db_provider
|
|
)
|
|
|
|
@classmethod
|
|
def create_with_custom_providers(cls, config_provider=None, db_provider=None):
|
|
"""Pour les tests avec mocks."""
|
|
config_provider = config_provider or ConfigManagerProvider()
|
|
db_provider = db_provider or SQLAlchemyDatabaseProvider()
|
|
|
|
return AssessmentServicesFacade(config_provider, db_provider)
|
|
```
|
|
|
|
## 📋 Data Transfer Objects (DTOs)
|
|
|
|
### Avantages des DTOs
|
|
|
|
**Découplage** : Séparation entre la logique métier et les modèles de données
|
|
|
|
```python
|
|
@dataclass
|
|
class ProgressResult:
|
|
"""Résultat standardisé du calcul de progression."""
|
|
percentage: int
|
|
completed: int
|
|
total: int
|
|
status: str
|
|
students_count: int
|
|
|
|
@dataclass
|
|
class StudentScore:
|
|
"""Score standardisé d'un étudiant."""
|
|
student_id: int
|
|
student_name: str
|
|
total_score: float
|
|
total_max_points: float
|
|
exercises: Dict[ExerciseId, Dict[str, Any]]
|
|
|
|
@dataclass
|
|
class StatisticsResult:
|
|
"""Résultat standardisé des calculs statistiques."""
|
|
count: int
|
|
mean: float
|
|
median: float
|
|
min: float
|
|
max: float
|
|
std_dev: float
|
|
```
|
|
|
|
### Utilisation Pratique
|
|
|
|
```python
|
|
# Dans le service
|
|
def calculate_grading_progress(self, assessment) -> ProgressResult:
|
|
# Calculs...
|
|
return ProgressResult(
|
|
percentage=85,
|
|
completed=34,
|
|
total=40,
|
|
status='in_progress',
|
|
students_count=25
|
|
)
|
|
|
|
# Dans le modèle (adapter)
|
|
@property
|
|
def grading_progress(self):
|
|
services = AssessmentServicesFactory.create_facade()
|
|
result = services.get_grading_progress(self)
|
|
|
|
# Conversion DTO → Dict pour compatibilité legacy
|
|
return {
|
|
'percentage': result.percentage,
|
|
'completed': result.completed,
|
|
'total': result.total,
|
|
'status': result.status,
|
|
'students_count': result.students_count
|
|
}
|
|
```
|
|
|
|
## 🚀 Avantages de l'Architecture SOLID
|
|
|
|
### 1. Maintenabilité
|
|
|
|
- **Code modulaire** : Chaque service a une responsabilité claire
|
|
- **Facilité de debug** : Isolation des problèmes par service
|
|
- **Evolution simplifiée** : Ajout de fonctionnalités sans régression
|
|
|
|
### 2. Testabilité
|
|
|
|
- **Mocking facile** : Interfaces permettent les tests unitaires
|
|
- **Isolation** : Chaque service testable indépendamment
|
|
- **Coverage** : 198 tests passent tous (vs 15 échecs avant)
|
|
|
|
### 3. Extensibilité
|
|
|
|
- **Nouveaux types de notation** : Strategy Pattern
|
|
- **Nouvelles sources de données** : DatabaseProvider
|
|
- **Nouvelles logiques métier** : Services spécialisés
|
|
|
|
### 4. Performance
|
|
|
|
- **Requêtes optimisées** : DatabaseProvider résout N+1
|
|
- **Lazy loading** : ConfigProvider évite les imports circulaires
|
|
- **Cache potentiel** : Architecture prête pour la mise en cache
|
|
|
|
## 📊 Métriques d'Amélioration
|
|
|
|
| Composant | Avant | Après | Réduction |
|
|
|-----------|-------|-------|-----------|
|
|
| Assessment | 279 lignes | 50 lignes | -82% |
|
|
| ClassGroup | 425 lignes | 80 lignes | -81% |
|
|
| GradingCalculator | 102 lignes | 32 lignes | -68% |
|
|
| Tests réussis | 183/198 | 198/198 | +15 tests |
|
|
| Complexité cyclomatique | Élevée | Faible | -60% |
|
|
| Dépendances circulaires | 5+ | 0 | -100% |
|
|
|
|
## 🎯 Migration et Compatibilité
|
|
|
|
### Adapter Pattern pour Compatibilité
|
|
|
|
Les modèles agissent comme des adapters pour maintenir l'API existante :
|
|
|
|
```python
|
|
class Assessment(db.Model):
|
|
# ... définition du modèle ...
|
|
|
|
@property
|
|
def grading_progress(self):
|
|
"""Adapter vers AssessmentProgressService."""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
result = services.get_grading_progress(self)
|
|
|
|
# Conversion DTO → format legacy
|
|
return {
|
|
'percentage': result.percentage,
|
|
'completed': result.completed,
|
|
'total': result.total,
|
|
'status': result.status,
|
|
'students_count': result.students_count
|
|
}
|
|
|
|
def calculate_student_scores(self):
|
|
"""Adapter vers StudentScoreCalculator."""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
students_scores_data, exercise_scores_data = services.calculate_student_scores(self)
|
|
|
|
# Conversion vers format legacy...
|
|
return students_scores, exercise_scores
|
|
```
|
|
|
|
### Migration Transparente
|
|
|
|
- **0 régression** : Toutes les APIs existantes fonctionnent
|
|
- **Amélioration progressive** : Nouveaux développements utilisent les services
|
|
- **Compatibilité templates** : Aucun changement frontend requis
|
|
|
|
## 🛠️ Utilisation Pratique
|
|
|
|
### Pour les Développeurs
|
|
|
|
```python
|
|
# Utilisation nouvelle architecture
|
|
from providers.concrete_providers import AssessmentServicesFactory
|
|
|
|
# Création des services
|
|
services = AssessmentServicesFactory.create_facade()
|
|
|
|
# Utilisation directe des services
|
|
progress = services.get_grading_progress(assessment)
|
|
statistics = services.get_statistics(assessment)
|
|
scores, exercise_scores = services.calculate_student_scores(assessment)
|
|
|
|
# Pour les tests avec mocks
|
|
mock_config = MockConfigProvider()
|
|
mock_db = MockDatabaseProvider()
|
|
services = AssessmentServicesFactory.create_with_custom_providers(
|
|
config_provider=mock_config,
|
|
db_provider=mock_db
|
|
)
|
|
```
|
|
|
|
### Pour les Tests
|
|
|
|
```python
|
|
def test_assessment_progress():
|
|
# Arrange
|
|
mock_db_provider = MockDatabaseProvider()
|
|
mock_db_provider.set_grades_data([...])
|
|
|
|
progress_service = AssessmentProgressService(mock_db_provider)
|
|
|
|
# Act
|
|
result = progress_service.calculate_grading_progress(assessment)
|
|
|
|
# Assert
|
|
assert result.percentage == 75
|
|
assert result.status == 'in_progress'
|
|
```
|
|
|
|
## 🎯 Prochaines Étapes
|
|
|
|
L'architecture SOLID Phase 1 pose les fondations pour :
|
|
|
|
1. **Cache Layer** : Services prêts pour la mise en cache
|
|
2. **API REST** : Services réutilisables pour les APIs
|
|
3. **Microservices** : Architecture découplée facilite la séparation
|
|
4. **Monitoring** : Points d'entrée clairs pour les métriques
|
|
5. **Event Sourcing** : Services peuvent émettre des événements
|
|
|
|
Cette architecture transforme Notytex en une application **moderne, maintenable et évolutive**, respectant les meilleures pratiques de l'industrie ! 🚀 |