La mise en œuvre d'une architecture applicative en trois couches est une pratique courante pour organiser le code, améliorer la modularité et faciliter la maintenance. Ce modèle sépare les responsabilités en distinctes couches : la couche de présentation (souvent un contrôleur), la couche de logique métier (le service) et la couche d'accès aux données (le mappeur ou DAO). L'approche recommandée pour l'utilisation d'interfaces et de classes concrètes diffère pour chaque composant.
| Couche | Interface requise ? | Implémentation requise ? | Raison principale |
|---|---|---|---|
| Contrôleur | Rarement | Oui | Le contrôleur est le point d'entrée des requêtes, il mappe une URL à une logique spécifique sans besoin d'abstraction pour des implémentations alternatives. |
| Service | Recommandé | Oui | La couche de service contient la logique métier. L'utilisation d'une interface assure le découplage, facilite les extensions (ex: multi-sources de données, règles métier diverses) et la testabilité. |
| Mappeur | Oui (Interface) | Non (manuelle) | Avec MyBatis, le framework génère automatiquement l'implémentation de l'interface du mappeur via un proxy dynamique, vous ne définissez que les méthodes. |
La Couche Contrôleur : Implémentations concrètes seulement
Pour la couche de présentation, il est d'usage de définir uniquement des classes concrètes, sans interfaces associées. - Approche habituelle : Créer directement des classes annotées avec @RestController, comme ControleurUtilisateurs, sans définir d'interface telle que InterfaceControleurUtilisateurs.
- Justification :
- Un contrôleur est le gestionnaire direct des requêtes HTTP. Chaque contrôleur est lié à des chemins d'URL spécifiques (par exemple,
/api/utilisateurs). Une URL correspond généralement à une seule logique de traitement, rendant superflue l'idée de "multiples implémentations" pour un même point d'accès. - L'ajout d'une interface à cette couche introduirait une redondance de code sans bénéfice significatif, ce qui n'est pas une pratique courante dans l'industrie.
- Un contrôleur est le gestionnaire direct des requêtes HTTP. Chaque contrôleur est lié à des chemins d'URL spécifiques (par exemple,
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// POJO simple pour les exemples
public class DonneeUtilisateur {
private Long identifiant;
private String nomUtilisateur;
// Constructeurs, Getters et Setters
public DonneeUtilisateur() {}
public DonneeUtilisateur(Long identifiant, String nomUtilisateur) {
this.identifiant = identifiant;
this.nomUtilisateur = nomUtilisateur;
}
public Long getIdentifiant() { return identifiant; }
public void setIdentifiant(Long identifiant) { this.identifiant = identifiant; }
public String getNomUtilisateur() { return nomUtilisateur; }
public void setNomUtilisateur(String nomUtilisateur) { this.nomUtilisateur = nomUtilisateur; }
}
@RestController
@RequestMapping("/api/utilisateurs")
public class ControleurUtilisateurs {
// Injection de dépendance via le constructeur est une bonne pratique
private final ServiceGestionUtilisateurs serviceUtilisateurs;
public ControleurUtilisateurs(ServiceGestionUtilisateurs serviceUtilisateurs) {
this.serviceUtilisateurs = serviceUtilisateurs;
}
@GetMapping
public ResponseEntity<List<DonneeUtilisateur>> obtenirListeGlobale() {
List<DonneeUtilisateur> utilisateurs = serviceUtilisateurs.recupererTousLesComptes();
return ResponseEntity.ok(utilisateurs); // Retourne une réponse HTTP 200 OK avec la liste
}
// Autres méthodes CRUD...
}
Les annotations comme @RestController sur la classe et @GetMapping sur les méthodes sont cruciales pour lier les requêtes HTTP au code applicatif. L'injection de la dépendance ServiceGestionUtilisateurs se fait via @Autowired (implicite avec l'injection par constructeur dans les versions récentes de Spring) ou explicitement. ### La Couche Service : L'approche Interface + Implémentation
La couche de service est le cœur de la logique métier, et c'est là que l'utilisation d'interfaces prend tout son sens. - Approche habituelle : Définir une interface, par exemple ServiceGestionUtilisateurs, puis créer une classe d'implémentation, telle que ServiceGestionUtilisateursImpl, annotée avec @Service.
- Justification :
- Découplage : L'interface définit "quoi faire" tandis que l'implémentation spécifie "comment le faire". Si la logique métier doit évoluer, une nouvelle implémentation (ex:
ServiceGestionUtilisateursV2Impl) peut être créée sans affecter le code du contrôleur qui interagit toujours avec l'interface. - Facilité d'extension : Pour des fonctionnalités complexes, comme un service de paiement, l'interface
ServicePaiementpourrait avoir plusieurs implémentations (ServicePaiementAlipayImpl,ServicePaiementStripeImpl). Le choix de l'implémentation est alors une simple question de configuration Spring. - Testabilité améliorée : Les interfaces facilitent grandement les tests unitaires et d'intégration en permettant l'utilisation de frameworks de mocking pour simuler le comportement du service sans dépendre de l'implémentation réelle ou de ses dépendances.
- Découplage : L'interface définit "quoi faire" tandis que l'implémentation spécifie "comment le faire". Si la logique métier doit évoluer, une nouvelle implémentation (ex:
import java.util.List;
import org.springframework.stereotype.Service;
// 1. Définition de l'interface (déclare les méthodes de logique métier)
public interface ServiceGestionUtilisateurs {
List<DonneeUtilisateur> recupererTousLesComptes();
// Autres méthodes métier...
}
// 2. Implémentation de la logique (annotée avec @Service pour être gérée par Spring)
@Service("servicePrincipalUtilisateurs") // Nomme le bean Spring
public class ServiceGestionUtilisateursImpl implements ServiceGestionUtilisateurs {
// Injection de dépendance du mappeur
private final MappeurDonneesUtilisateur mappeurUtilisateur;
public ServiceGestionUtilisateursImpl(MappeurDonneesUtilisateur mappeurUtilisateur) {
this.mappeurUtilisateur = mappeurUtilisateur;
}
@Override
public List<DonneeUtilisateur> recupererTousLesComptes() {
// Appelle la couche d'accès aux données
return mappeurUtilisateur.trouverTousLesEnregistrements();
}
}
La Couche Mappeur (DAO) : Interafces uniquement, implémentation automatique
La couche d'accès aux données, souvent appelée DAO (Data Access Object) ou Mappeur dans le contexte de MyBatis, se concentre sur les interactions avec la base de données. - Approche habituelle : Définir uniquement une interface, par exemple MappeurDonneesUtilisateur, annotée avec @Mapper (pour MyBatis), sans créer de classe d'implémentation manuelle.
- Justification :
- Un mappeur est spécifiquement conçu pour lier des opérations de données à des requêtes SQL. MyBatis exploite la "programmation par proxy dynamique" : il génère automatiquement une implémentation concrète de l'interface du mappeur au moment de l'exécution. Cette implémentation s'occupe de toute la logique JDBC sous-jacente.
- Écrire manuellement une classe d'implémentation pour le mappeur reviendrait à réintroduire le boilerplate code de JDBC, annulant ainsi l'un des principaux avantages de frameworks ORM comme MyBatis.
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
// Seule l'interface est nécessaire, MyBatis générera l'implémentation
@Mapper // Indique à MyBatis de générer un proxy pour cette interface
public interface MappeurDonneesUtilisateur {
// Lie une méthode à une requête SQL spécifique
@Select("SELECT identifiant, nom_utilisateur FROM table_utilisateurs")
List<DonneeUtilisateur> trouverTousLesEnregistrements();
// Autres opérations (INSERT, UPDATE, DELETE)...
}
En résumé, l'objectif de la couche DAO/Mappeur est d'isoler toutes les interactions avec la base de données, fournissant à la couche service une API claire et abstraite pour manipuler les données. MyBatis simplifie grandement ce processus en éliminant la nécessité d'écrire le code d'implémentation, permettant aux développeurs de se concnetrer sur la définition des opérations de données via des interfaces et des annotations SQL.