Patron Singleton : Gestion d'instance unique
Keine KartenLe patron de conception Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à celle-ci. Ceci est utile lorsque vous avez besoin d'une ressource unique, comme une connexion à une base de données ou une configuration globale. Les sources 6, 8 et 7 illustrent la mise en œuvre et l'utilisation de ce patron.
Exemples de Patrons de Conception
Les patrons de conception sont des solutions réutilisables à des problèmes de conception de logiciels courants. Ils servent de modèles pour structurer le code afin d'améliorer sa flexibilité, sa maintenabilité et sa réutilisabilité.
1. Patron Singleton
Le patron Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à cette instance.
Problématique sans Singleton
Considérons un programme de gestion de base de données où plusieurs parties du code ont besoin d'accéder à une instance de connexion à la base de données (Source 1).
public class DatabaseConnection {
public DatabaseConnection() {
System.out.println("Connexion à la base de données créée.");
}
}Si plusieurs parties de l'application créent leur propre connexion, comme dans cet exemple (Source 3, Source 4) :
public class Application {
public static void main(String[] args) {
DatabaseConnection conn1 = new DatabaseConnection();
DatabaseConnection conn2 = new DatabaseConnection();
DatabaseConnection conn3 = new DatabaseConnection();
}
}À chaque appel au constructeur DatabaseConnection(), une nouvelle connexion à la base de données est créée. Cela entraîne un gaspillage de ressources système et peut provoquer des problèmes, comme un dépassement de connexions si trop de connexions sont ouvertes simultanément (Source 5).
Solution avec Singleton
Le patron Singleton résout ce problème en s'assurant qu'une seule instance de DatabaseConnection est créée (Source 6).
public class DatabaseConnection {
private static DatabaseConnection instance; // Unique instance
private DatabaseConnection() { // Constructeur privé
System.out.println("Connexion à la base de données créée.");
}
public static DatabaseConnection getInstance() { // Méthode d'accès globale
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}}
Le constructeur est privé pour empêcher toute instanciation directe.
Une instance statique privée est déclarée pour contenir l'unique instance de la classe.
Une méthode statique publique (
getInstance()) fournit le point d'accès global à l'instance. Elle crée l'instance si elle n'existe pas déjà et la retourne (Source 8).
Utilisation du Singleton :
public class Application {
public static void main(String[] args) {
DatabaseConnection conn1 = DatabaseConnection.getInstance();
DatabaseConnection conn2 = DatabaseConnection.getInstance();
DatabaseConnection conn3 = DatabaseConnection.getInstance();
conn1.connect(); // Exemple d'utilisation (Source 7)
System.out.println(conn1 == conn2); // true
System.out.println(conn2 == conn3); // true
}}
Toutes les variables (conn1, conn2, conn3) pointent vers la même instance de DatabaseConnection, garantissant une seule connexion active.
2. Patron Stratégie (Strategy)
Le patron Stratégie permet de définir une famille d'algorithmes, de les encapsuler et de les rendre interchangeables. La stratégie permet de modifier l'algorithme indépendamment des clients qui l'utilisent.
Problématique sans Stratégie
Illustrons avec l'exemple d'une boutique en ligne appliquant différentes stratégies de réduction (Source 9).
public class Cart {
private double totalAmount;
private String discountType; // (NoDiscount, FestivalDiscount, VIPDiscount)
public Cart(double totalAmount, String discountType) {
this.totalAmount = totalAmount;
this.discountType = discountType;
}
public double calculateFinalAmount() {
if (discountType.equals("FestivalDiscount")) {
return totalAmount * 0.9; // 10% de réduction
} else if (discountType.equals("VIPDiscount")) {
return totalAmount * 0.8; // 20% de réduction
} else {
return totalAmount; // Pas de réduction
}
}}
L'utilisation serait la suivante (Source 11) :
public class Application {
public static void main(String[] args) {
Cart cart1 = new Cart(100, "FestivalDiscount");
System.out.println("Prix final (réduction fêtes) : " + cart1.calculateFinalAmount()); // Output: 90.0
Cart cart2 = new Cart(100, "VIPDiscount");
System.out.println("Prix final (réduction VIP) : " + cart2.calculateFinalAmount()); // Output: 80.0
}
}Cette approche présente deux inconvénients majeurs (Source 12) :
Manque de flexibilité : Pour ajouter une nouvelle réduction (ex: réduction de fin d'année), il faudrait modifier la méthode
calculateFinalAmount(). Cela viole le principe d'ouverture/fermeture (OCP).Code complexe : Plus le nombre de types de réduction augmente, plus la méthode
calculateFinalAmount()devient complexe et difficile à maintenir.
Solution avec Stratégie
Le patron Stratégie externalise les algorithmes dans des classes séparées (Source 13).
// Interface définissant la méthode commune pour les stratégies de réduction
public interface IDiscountStrategy {
double applyDiscount(double amount);
}
// Implémentation de la stratégie sans réduction
public class NoDiscount implements IDiscountStrategy {
@Override
public double applyDiscount(double amount) {
return amount;
}
}
// Implémentation de la stratégie pour une réduction des fêtes
public class FestivalDiscount implements IDiscountStrategy {
@Override
public double applyDiscount(double amount) {
return amount * 0.9; // 10% de réduction
}
}
// Implémentation de la stratégie pour une réduction VIP
public class VIPDiscount implements IDiscountStrategy {
@Override
public double applyDiscount(double amount) {
return amount * 0.8; // 20% de réduction
}
}
La classe Cart utilise désormais une interface de stratégie (Source 15) :
public class Cart {
private double totalAmount;
private IDiscountStrategy discountStrategy; // Utilisation de la stratégie
public Cart(double totalAmount, IDiscountStrategy discountStrategy) {
this.totalAmount = totalAmount;
this.discountStrategy = discountStrategy;
}
public double calculateFinalAmount() {
return discountStrategy.applyDiscount(totalAmount); // Délégation à la stratégie
}}
Utilisation de la Stratégie :
public class Application {
public static void main(String[] args) {
// Application de la stratégie FestivalDiscount
Cart cart1 = new Cart(100, new FestivalDiscount());
System.out.println("Prix final (réduction fêtes) : " + cart1.calculateFinalAmount()); // Output: 90.0
// Application de la stratégie VIPDiscount
Cart cart2 = new Cart(100, new VIPDiscount());
System.out.println("Prix final (réduction VIP) : " + cart2.calculateFinalAmount()); // Output: 80.0
// Application de la stratégie NoDiscount
Cart cart3 = new Cart(100, new NoDiscount());
System.out.println("Prix final (sans réduction) : " + cart3.calculateFinalAmount()); // Output: 100.0
}}
L'ajout d'une nouvelle stratégie (ex: ChristmasDiscount) ne nécessite pas de modifier la classe Cart, respectant ainsi l'OCP (Source 16).
3. Patron Composite
Le patron Composite permet de composer des objets en structures arborescentes pour représenter des hiérarchies partie-tout. Le client peut traiter les objets individuels et les compositions d'objets de manière uniforme.
Problématique sans Composite
Considérons un système de fichiers avec des fichiers et des dossiers. Nous voulons traiter ces deux types d'éléments de manière uniforme (Source 17).
import java.util.ArrayList;
import java.util.List;
// Classe pour les Fichiers
public class File {
private String name;
public File(String name) {
this.name = name;
}
public void display() {
System.out.println("Fichier : " + name);
}
}
// Classe pour les Dossiers
public class Folder {
private String name;
private List<File> files = new ArrayList<>(); // Ne gère que les fichiers
public Folder(String name) {
this.name = name;
}
public void addFile(File file) {
files.add(file);
}
public void display() {
System.out.println("Dossier : " + name);
for (File file : files) {
file.display();
}
}
}
Exemple d'utilisation (Source 19) :
public class Application {
public static void main(String[] args) {
File file1 = new File("Document1.txt");
File file2 = new File("Document2.txt");
Folder folder = new Folder("Mes Documents");
folder.addFile(file1);
folder.addFile(file2);
folder.display();
}}
Cette approche présente des limites (Source 20) :
Manque de flexibilité : Pour ajouter la gestion de sous-dossiers, la classe
Folderdevrait être modifiée pour gérer à la foisFileetFolder, ce qui complexifie le code.Difficulté de récursivité : Il n'y a pas de moyen simple et uniforme de parcourir les fichiers et les dossiers, car chaque type d'objet est géré séparément.
Solution avec Composite
Le patron Composite unifie le traitement des éléments individuels et composites (Source 21).
// Interface commune pour les fichiers et dossiers (Composant)
public interface IFileSystemComponent {
void display();
}
// Classe File (Feuille)
public class File implements IFileSystemComponent {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void display() {
System.out.println("Fichier : " + name);
}
}
// Classe Folder (Composite)
public class Folder implements IFileSystemComponent {
private String name;
private List<IFileSystemComponent> components = new ArrayList<>(); // Liste de composants
public Folder(String name) {
this.name = name;
}
public void addComponent(IFileSystemComponent component) { // Ajout de fichier ou sous-dossier
components.add(component);
}
public void removeComponent(IFileSystemComponent component) {
components.remove(component);
}
@Override
public void display() {
System.out.println("Dossier : " + name);
for (IFileSystemComponent component : components) {
component.display(); // Afficher chaque composant récursivement
}
}
}
Utilisation du Composite (Source 24) :
public class Application {
public static void main(String[] args) {
File file1 = new File("Document1.txt");
File file2 = new File("Document2.txt");
File file3 = new File("Photo.jpg");
Folder folder1 = new Folder("Mes Documents");
folder1.addComponent(file1);
folder1.addComponent(file2);
Folder folder2 = new Folder("Mon Bureau");
folder2.addComponent(folder1); // Ajout d'un dossier à un autre dossier
folder2.addComponent(file3);
System.out.println("Affichage de la structure de 'Mon Bureau' :");
folder2.display();
}}
Grâce à l'interface IFileSystemComponent, les fichiers et les dossiers sont traités uniformément, permettant une structure hiérarchique complexe et une traversée récursive simplifiée (Source 25).
4. Patron Observateur (Observer)
Le patron Observateur définit une dépendance un-à-plusieurs entre objets, de telle sorte que lorsqu'un objet change d'état, tous ses dépendants sont avertis et mis à jour automatiquement.
Problématique sans Observateur
Imaginons un système où plusieurs panneaux d'affichage (afficheurs) doivent réagir aux changements de température d'une station météo (Source 26).
// Classe StationMeteo qui contient la température
public class StationMeteo {
private double temperature;
public double getTemperature() { return temperature; }
public void setTemperature(double temperature) { this.temperature = temperature; }
}
// Classe pour un Afficheur numérique
public class AfficheurNumerique {
private StationMeteo station;
public AfficheurNumerique(StationMeteo station) { this.station = station; }
public void afficher() {
System.out.println("Afficheur Numérique : Température actuelle : " + station.getTemperature());
}
}
// Classe pour un Afficheur analogique
public class AfficheurAnalogique {
private StationMeteo station;
public AfficheurAnalogique(StationMeteo station) { this.station = station; }
public void afficher() {
System.out.println("Afficheur Analogique : Température actuelle : " + station.getTemperature());
}
}
Cette approche présente les inconvénients suivants (Source 28) :
Couplage fort : Chaque afficheur dépend directement de la classe
StationMeteo. Toute modification deStationMeteopourrait impacter tous les afficheurs.Manque de réactivité : Les afficheurs doivent interroger (pull) la station météo pour obtenir les mises à jour, ce qui n'est pas efficace si l'état change fréquemment.
Solution avec Observateur
Le patron Observateur établit une relation de publication/abonnement (Source 29).
// Interface pour les observateurs (Afficheurs)
public interface IObservateur {
void mettreAJour();
}
// Classe abstraite Sujet (Observable)
import java.util.ArrayList;
import java.util.List;
public abstract class Sujet {
private List<IObservateur> observateurs = new ArrayList<IObservateur>();
public void ajouterObservateur(IObservateur observateur) {
observateurs.add(observateur);
}
public void supprimerObservateur(IObservateur observateur) {
observateurs.remove(observateur);
}
public void notifierObservateurs() { // Méthode pour notifier les observateurs
for (IObservateur observateur : observateurs) {
observateur.mettreAJour();
}
}}
// Classe pour la StationMeteo (Sujet concret)
public class StationMeteo extends Sujet {
private double temperature;
public void setTemperature(double temperature) {
this.temperature = temperature;
notifierObservateurs(); // Notifie les observateurs lors du changement
}
public double getTemperature() { return temperature; }
}
// Classes d'afficheurs (Observateurs concrets)
public class AfficheurAnalogique implements IObservateur {
private StationMeteo sujet;
public AfficheurAnalogique(StationMeteo sujet) {
this.sujet = sujet;
sujet.ajouterObservateur(this); // S'abonne au sujet
this.mettreAJour(); // Affichage initial
}
@Override
public void mettreAJour() {
System.out.println("Afficheur Analogique : Température actuelle : " + sujet.getTemperature());
}
}
public class AfficheurNumerique implements IObservateur {
private StationMeteo sujet;
public AfficheurNumerique(StationMeteo sujet) {
this.sujet = sujet;
sujet.ajouterObservateur(this); // S'abonne au sujet
this.mettreAJour(); // Affichage initial
}
@Override
public void mettreAJour() {
System.out.println("Afficheur Numérique : Température actuelle : " + sujet.getTemperature());
}
}
Utilisation du patron Observateur (Source 34) :
public class Application {
public static void main(String[] args) {
StationMeteo station = new StationMeteo();
AfficheurNumerique afficheurNum = new AfficheurNumerique(station);
AfficheurAnalogique afficheurAna = new AfficheurAnalogique(station);
station.setTemperature(25.0); // Les afficheurs seront notifiés automatiquement
station.setTemperature(30.0); // Et mis à jour
}}
/* Output attendu:
Afficheur Numérique : Température actuelle : 0.0
Afficheur Analogique : Température actuelle : 0.0
Afficheur Numérique : Température actuelle : 25.0
Afficheur Analogique : Température actuelle : 25.0
Afficheur Numérique : Température actuelle : 30.0
Afficheur Analogique : Température actuelle : 30.0
*/
Désormais, la StationMeteo (sujet) notifie automatiquement ses Observateurs (afficheurs) de tout changement, réduisant le couplage et améliorant la réactivité (Source 35).
Récapitulatif des Patrons
Patron | Problème résolu | Avantages | Exemple |
Singleton | Gestion d'une unique instance d'une classe. | Contrôle strict de l'instanciation, économie de ressources. | Connexion à une base de données, logger unique. |
Stratégie | Variété d'algorithmes interchangeables pour un même comportement. | Flexibilité, respect du Principe Ouvert/Fermé (OCP), code plus propre. | Calcul de réductions, algorithmes de tri. |
Composite | Traitement uniforme d'objets individuels et de leurs compositions. | Simplification du code client, gestion facile des structures arborescentes. | Système de fichiers, composants d'interface utilisateur. |
Observateur | Notification de dépendants suite à un changement d'état d'un objet. | Couplage faible, réactivité, système de publication/abonnement. | Station météo et afficheurs, événements GUI. |
Quiz starten
Teste dein Wissen mit interaktiven Fragen