CHAPITRE 9 : LA COUCHE SERVICE ET LA LOGIQUE METIER

Le service est le cœur de l'application : c'est la que vivent les regles de votre metier. Bien le concevoir evite qu'une application devienne ingerable.

1. Pourquoi une couche service ?

Sans elle, la logique se disperse : un peu dans le controleur, un peu dans le repository, parfois dupliquee. Le jour ou une regle change, il faut la retrouver partout.

CoucheRepond a
ControllerComment recevoir et renvoyer ?
ServiceQue faut-il faire ?
RepositoryComment lire et ecrire ?

2. Un service complet

@Service
@Transactional(readOnly = true)
public class EtudiantService {

    private final EtudiantRepository repository;
    private final FiliereRepository  filiereRepository;

    public EtudiantService(EtudiantRepository repository,
                           FiliereRepository filiereRepository) {
        this.repository = repository;
        this.filiereRepository = filiereRepository;
    }

    public List<Etudiant> listerTous() {
        return repository.findAll();
    }

    public Etudiant trouverParId(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new EtudiantIntrouvableException(id));
    }

    @Transactional
    public Etudiant inscrire(InscriptionDTO dto) {

        if (repository.findByMatricule(dto.matricule()).isPresent()) {
            throw new MatriculeDejaUtiliseException(dto.matricule());
        }

        Filiere filiere = filiereRepository.findById(dto.filiereId())
                .orElseThrow(() -> new FiliereIntrouvableException(dto.filiereId()));

        if (filiere.getEtudiants().size() >= filiere.getCapacite()) {
            throw new FiliereCompleteException(filiere.getLibelle());
        }

        Etudiant etudiant = new Etudiant(dto.nom(), dto.matricule(), 0.0);
        etudiant.setFiliere(filiere);

        return repository.save(etudiant);
    }
}

Observez ce que fait cette methode : elle verifie des regles (matricule unique, filiere existante, capacite disponible) avant d'enregistrer. Aucune de ces regles n'a sa place dans un controleur.

3. Les transactions

Une transaction garantit qu'un ensemble d'operations reussit entierement, ou echoue entierement.

@Transactional
public void transferer(Long etudiantId, Long nouvelleFiliereId) {

    Etudiant etudiant = trouverParId(etudiantId);
    Filiere  ancienne = etudiant.getFiliere();
    Filiere  nouvelle = filiereRepository.findById(nouvelleFiliereId)
                            .orElseThrow();

    ancienne.retirer(etudiant);
    nouvelle.ajouter(etudiant);       // si cette ligne echoue,
    etudiant.setFiliere(nouvelle);    // le retrait est annule
}

Deux pieges a connaitre

  • Par defaut, seule une RuntimeException declenche l'annulation. Une exception verifiee ne le fait pas, sauf a preciser @Transactional(rollbackFor = Exception.class).
  • Un appel interne ne passe pas par le proxy. Si une methode de la classe en appelle une autre annotee @Transactional, l'annotation est ignoree. C'est une source de bugs tres difficiles a diagnostiquer.

4. Les exceptions metier

public class EtudiantIntrouvableException extends RuntimeException {

    public EtudiantIntrouvableException(Long id) {
        super("Aucun etudiant avec l'identifiant " + id);
    }
}

Des exceptions nommees valent bien mieux qu'un return null : elles disent ce qui s'est passe, et le chapitre suivant montrera comment les transformer en reponses HTTP propres.

5. Le mapping DTO

Quand la conversion devient repetitive, isolez-la dans une classe dediee :

@Component
public class EtudiantMapper {

    public EtudiantDTO versDTO(Etudiant e) {
        return new EtudiantDTO(
            e.getId(),
            e.getNom(),
            e.getMatricule(),
            e.getMoyenne(),
            e.getFiliere() != null ? e.getFiliere().getLibelle() : null
        );
    }

    public List<EtudiantDTO> versDTO(List<Etudiant> etudiants) {
        return etudiants.stream().map(this::versDTO).toList();
    }
}

6. Interface ou classe concrete ?

Beaucoup de tutoriels creent systematiquement une interface EtudiantService et une implementation EtudiantServiceImpl. C'est rarement justifie : une interface n'a de sens que s'il existe plusieurs implementations, ou pour un besoin de test precis.

Un conseil pragmatique : commencez par une classe concrete. Vous extrairez une interface le jour ou elle devient necessaire, ce qui n'arrive pas si souvent.

A retenir

  • Le service porte les regles de gestion
  • Controleur mince, service epais
  • Seules les RuntimeException annulent une transaction par defaut
  • Un appel interne ignore @Transactional
  • N'ajoutez une interface que si elle sert vraiment

Exercices d'application

Exercice 1. Une regle metierAjoutez une regle : impossible d'inscrire un etudiant si la filiere a atteint sa capacite. Levez une exception dediee et testez le cas.
Exercice 2. TransactionEcrivez une methode qui modifie deux entites. Provoquez volontairement une erreur sur la seconde et verifiez que la premiere est bien annulee.
Exercice 3. MapperCreez un CoursMapper qui convertit l'entite en DTO, en incluant le nombre d'etudiants inscrits.

Cherchez avant de regarder la solution : c'est en butant sur les difficultes que l'on progresse.

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam