Chapitre 1: La Couche Service en Détail
Introduction
La couche Service est l'élément central de l'architecture à 3 couches. Elle fait le lien entre le contrôleur (qui gère les requêtes HTTP) et le repository (qui accède aux données). C'est ici que réside la logique métier de votre application.
Objectifs du Chapitre
À la fin de ce chapitre, vous serez capable de:
- Comprendre le rôle et les responsabilités de la couche Service
- Séparer correctement les préoccupations entre les couches
- Implémenter une couche Service professionnelle
- Utiliser l'injection de dépendances correctement
- Appliquer les bonnes pratiques
Architecture à 3 Couches
Vue d'Ensemble
Responsabilités de Chaque Couche
| Couche | Responsabilités | Exemples |
|---|---|---|
| Controller | Réception HTTP, Validation, Conversion, Réponses | @RestController, @GetMapping |
| Service | Logique métier, Orchestration, Transactions, Validation règles | @Service, Calculs |
| Repository | Accès données, Requêtes BD, Mapping entités | @Repository, JpaRepository |
Qu'est-ce que la Logique Métier?
Définition
La logique métier (business logic) est l'ensemble des règles et processus qui définissent le comportement de votre application.
Exemples de Logique Métier
❌ PAS de la logique métier:
// Ceci est de l'accès aux données
productRepository.findById(id);
// Ceci est de la gestion HTTP
return ResponseEntity.ok(product);
✅ Logique métier:
// Vérifier le stock avant de créer une commande
if (product.getStock() < order.getQuantity()) {
throw new InsufficientStockException();
}
// Calculer le prix total avec taxes
double totalPrice = order.getSubtotal() * (1 + TAX_RATE);
// Appliquer une promotion
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9; // 10% réduction
}
Création d'une Couche Service
Exemple Basique
package com.example.demo.service;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
// Injection par constructeur (RECOMMANDÉ)
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// CREATE
public Product createProduct(Product product) {
// Logique métier: validation
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
// Appel au repository
return productRepository.save(product);
}
// READ - Tous
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// READ - Par ID
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
}
// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
// Logique métier: validation
if (productDetails.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
// Mise à jour des champs
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
// DELETE
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
Points Clés
- @Service: Marque la classe comme une couche Service
- Injection par constructeur: Plus propre et testable que @Autowired sur les champs
- Validation métier: Vérification des règles avant l'accès aux données
- Gestion des erreurs: Lancer des exceptions appropriées
Injection de Dépendances
Trois Méthodes d'Injection
1. Injection par Constructeur (RECOMMANDÉE)
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Avantages:
- ✅ Immutabilité (final)
- ✅ Testable facilement
- ✅ Obligatoire (compile error si manquant)
- ✅ Recommandé par Spring
2. Injection par Champ (NON RECOMMANDÉE)
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
}
Inconvénients:
- ❌ Non testable facilement
- ❌ Dépendances cachées
- ❌ Pas immutable
3. Injection par Setter (RAREMENT UTILISÉE)
@Service
public class ProductService {
private ProductRepository productRepository;
@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Séparation des Préoccupations
Principe SOLID: Single Responsibility
Chaque couche doit avoir UNE seule responsabilité.
❌ Mauvais Exemple: Tout dans le Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@PostMapping
public Product create(@RequestBody Product product) {
// Logique métier dans le controller (MAUVAIS!)
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
// Accès direct au repository (MAUVAIS!)
return productRepository.save(product);
}
}
Problèmes:
- ❌ Logique métier dans le controller
- ❌ Difficile à tester
- ❌ Difficile à réutiliser
- ❌ Violation du principe de responsabilité unique
✅ Bon Exemple: Séparation Claire
// Controller: Gestion HTTP uniquement
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public Product create(@RequestBody Product product) {
return productService.createProduct(product);
}
}
// Service: Logique métier
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product createProduct(Product product) {
// Validation métier
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
// Appel au repository
return productRepository.save(product);
}
}
// Repository: Accès aux données
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring génère automatiquement l'implémentation
}
Exemple Avancé: Orchestration
La couche Service peut coordonner plusieurs repositories:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
}
public Order createOrder(Long customerId, Long productId, int quantity) {
// 1. Vérifier que le client existe
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));
// 2. Vérifier que le produit existe
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
// 3. Vérifier le stock
if (product.getStock() < quantity) {
throw new InsufficientStockException("Stock insuffisant");
}
// 4. Calculer le prix total
double totalPrice = product.getPrice() * quantity;
// 5. Appliquer une réduction si client premium
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9;
}
// 6. Créer la commande
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);
// 7. Diminuer le stock
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// 8. Sauvegarder la commande
return orderRepository.save(order);
}
}
Gestion des Transactions
Annotation @Transactional
@Service
public class OrderService {
@Transactional
public Order createOrder(Long customerId, Long productId, int quantity) {
// Toutes les opérations sont dans une transaction
// Si une exception est lancée, tout est annulé (rollback)
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setStock(product.getStock() - quantity);
productRepository.save(product);
Order order = new Order();
// ... création commande
return orderRepository.save(order);
}
}
Avantages:
- ✅ Cohérence des données
- ✅ Rollback automatique en cas d'erreur
- ✅ ACID (Atomicité, Cohérence, Isolation, Durabilité)
Bonnes Pratiques
DO (À Faire)
✅ Injection par constructeur
public ProductService(ProductRepository repository) {
this.repository = repository;
}
✅ Méthodes avec noms explicites
public Product createProduct(Product product)
public void activateUser(Long userId)
public double calculateTotalPrice(Order order)
✅ Validation métier dans le Service
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}
✅ Gestion des exceptions appropriées
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
DON'T (À Éviter)
❌ Logique métier dans le Controller
// MAUVAIS: Dans le controller
@PostMapping
public Product create(@RequestBody Product product) {
if (product.getPrice() < 0) { // Logique métier!
throw new Exception();
}
return service.create(product);
}
❌ Accès direct au Repository depuis le Controller
// MAUVAIS
@RestController
public class ProductController {
@Autowired
private ProductRepository repository; // Direct!
}
❌ Injection par champ
// MAUVAIS
@Autowired
private ProductRepository repository;
Exercices Pratiques
Exercice 1: Service Basique
Objectif: Créer une couche Service complète
Instructions:
- Créez une entité
Student:- Long id
- String firstName
- String lastName
- String email
- int grade
- Créez
StudentRepository - Créez
StudentServiceavec:- Toutes les opérations CRUD
- Validation: grade entre 0 et 100
- Validation: email valide
- Créez
StudentControllerqui utilise le service
Exercice 2: Orchestration
Objectif: Coordonner plusieurs repositories
Instructions:
- Créez
Library(id, name, address) - Créez
Book(id, title, author, libraryId) - Créez
LibraryServiceavec une méthode:public Library createLibraryWithBooks(Library library, List<Book> books)- Créer la bibliothèque
- Créer tous les livres associés
- Tout dans une transaction
Résumé du Chapitre
Points Clés
- La couche Service contient la logique métier
- Séparation des préoccupations: Controller → Service → Repository
- Injection par constructeur (recommandée)
- @Transactional pour la cohérence des données
- Validation dans le Service, pas dans le Controller
Ce que Vous Savez Maintenant
✅ Rôle et responsabilités de la couche Service
✅ Séparation des préoccupations
✅ Injection de dépendances
✅ Orchestration de plusieurs repositories
✅ Bonnes pratiques professionnelles