Aller au contenu principal

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​

CoucheResponsabilitésExemples
ControllerRéception HTTP, Validation, Conversion, Réponses@RestController, @GetMapping
ServiceLogique métier, Orchestration, Transactions, Validation règles@Service, Calculs
RepositoryAccè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​

  1. @Service: Marque la classe comme une couche Service
  2. Injection par constructeur: Plus propre et testable que @Autowired sur les champs
  3. Validation métier: Vérification des règles avant l'accès aux données
  4. 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:

  1. Créez une entité Student:
    • Long id
    • String firstName
    • String lastName
    • String email
    • int grade
  2. Créez StudentRepository
  3. Créez StudentService avec:
    • Toutes les opérations CRUD
    • Validation: grade entre 0 et 100
    • Validation: email valide
  4. Créez StudentController qui utilise le service

Exercice 2: Orchestration​

Objectif: Coordonner plusieurs repositories

Instructions:

  1. Créez Library (id, name, address)
  2. Créez Book (id, title, author, libraryId)
  3. Créez LibraryService avec 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


Chapitre suivant: Validation des Données

Retour au Module 3