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.
Couche
Repond a
Controller
Comment recevoir et renvoyer ?
Service
Que faut-il faire ?
Repository
Comment 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.