1. L’abstraction dans les applications réelles : pourquoi en a-t-on besoin
Dans les cours précédents, nous avons déjà fait connaissance avec la notion d’abstraction et examiné des exemples simples. Voyons maintenant comment cette approche fonctionne dans des tâches plus réalistes. Dans les projets réels, on doit presque toujours traiter avec des objets différents mais similaires. Par exemple, différents moyens de paiement, différents types de transport, différentes formes dans un éditeur graphique. Sans abstraction, le code devient rapidement un ensemble de « if-else » et de copier-coller. Avec l’abstraction — tout est ordonné, élégant et, surtout, pratique pour l’évolution et la maintenance.
L’abstraction permet de :
- Cacher les détails d’implémentation : travailler avec des objets via une interface commune, sans se soucier de leur fonctionnement interne.
- Éviter la duplication de code : le comportement et les champs communs sont extraits dans une classe de base.
- Étendre facilement le système : l’ajout de nouveaux types d’objets ne nécessite pas de réécrire l’ancien code.
- Rendre le code flexible : on peut remplacer une implémentation par une autre sans modifier le reste du système.
Examinons quelques exemples de domaines différents.
2. Exemple 1 : Systèmes de paiement
Énoncé du problème
Supposons que vous écriviez un module pour une boutique en ligne. Votre tâche est d’implémenter le traitement de différents types de paiements : carte bancaire, PayPal, cryptomonnaie. Ils doivent tous « traiter un paiement », mais chacun a ses propres détails.
Abstraction : classe Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Méthode abstraite : la façon de traiter le paiement est décidée par les sous-classes
public abstract void process();
// Méthode commune à tous les paiements
public void printAmount() {
System.out.println("Montant du paiement : " + amount + " EUR");
}
}
Implémentations concrètes
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("Traitement du paiement par carte : " + cardNumber);
// Ici, on pourrait intégrer une banque :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("Traitement du paiement PayPal pour le compte : " + email);
// Et ici — appel de l’API PayPal
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("Traitement du paiement crypto vers le portefeuille : " + walletAddress);
// Ici, il pourrait y avoir la magie de la blockchain
}
}
Utilisation de l’abstraction
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
Résultat :
Montant du paiement : 1500.0 EUR
Traitement du paiement par carte : 1234 5678 9012 3456
---
Montant du paiement : 500.0 EUR
Traitement du paiement PayPal pour le compte : user@example.com
---
Montant du paiement : 0.05 EUR
Traitement du paiement crypto vers le portefeuille : 0xABCD...
---
Avantages :
- On peut ajouter un nouveau moyen de paiement sans modifier l’ancien code (par exemple, Apple Pay).
- Le code qui travaille avec les paiements ne dépend pas de leur type concret.
- La logique commune (par exemple, l’impression du montant via printAmount()) est implémentée en un seul endroit.
3. Exemple 2 : Transport
Énoncé du problème
Dans un jeu ou un simulateur, vous avez différents types de transport : voitures, vélos, trains. Ils peuvent tous « se déplacer », mais de manières différentes. Certains nécessitent du ravitaillement, d’autres non.
Abstraction : classe Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Tous les transports n’ont pas besoin de ravitaillement, mais par défaut — non
public void fuelUp() {
System.out.println(name + ": ravitaillement non requis.");
}
}
Implémentations concrètes
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " roule sur la route.");
}
@Override
public void fuelUp() {
System.out.println(name + ": on fait le plein d’essence.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " pédale.");
}
// On ne redéfinit pas fuelUp — un vélo n’a pas besoin de ravitaillement
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " file sur les rails.");
}
@Override
public void fuelUp() {
System.out.println(name + ": ravitaillement en diesel ou en électricité.");
}
}
Utilisation de l’abstraction
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Giant"),
new Train("Express")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
Résultat :
Toyota roule sur la route.
Toyota: on fait le plein d’essence.
---
Giant pédale.
Giant: ravitaillement non requis.
---
Express file sur les rails.
Express: ravitaillement en diesel ou en électricité.
---
Avantages :
- On peut traiter n’importe quel moyen de transport de la même manière, sans vérifier son type.
- Facile d’ajouter un nouveau type de transport (par exemple, une trottinette électrique).
4. Exemple 3 : Éditeur graphique
Énoncé du problème
Vous écrivez un mini-éditeur graphique. Il contient des lignes, des ellipses, des polygones — et tout cela sont des « formes » que l’on peut dessiner et redimensionner. Chaque forme implémente ces actions à sa manière.
Abstraction : classe Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
Implémentations concrètes
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("On dessine une ligne de longueur " + length + " de couleur " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Nouvelle longueur de la ligne : " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("On dessine une ellipse avec des axes " + a + " et " + b + " de couleur " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Nouvelles dimensions de l’ellipse : " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("On dessine un polygone avec " + sides + " côtés de couleur " + color);
}
@Override
public void resize(double factor) {
System.out.println("On redimensionne le polygone avec " + sides + " côtés à " + factor);
}
}
Utilisation de l’abstraction
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
Résultat :
On dessine une ligne de longueur 10.0 de couleur green
Nouvelle longueur de la ligne : 20.0
---
On dessine une ellipse avec des axes 5.0 et 3.0 de couleur green
Nouvelles dimensions de l’ellipse : 10.0 x 6.0
---
On dessine un polygone avec 6 côtés de couleur green
On redimensionne le polygone avec 6 côtés à 2.0
---
Avantages :
- Toutes les formes peuvent être stockées dans une seule liste et traitées de la même manière.
- Facile d’ajouter une nouvelle forme (par exemple, une étoile ou un cœur).
- Les méthodes communes (par exemple, définir la couleur via setColor()) sont implémentées une seule fois.
5. Comment l’abstraction aide à simplifier le code
Dans chaque exemple ci-dessus, on retrouve le même schéma :
- La classe abstraite de base définit le contrat (ce que l’objet sait faire).
- Les sous-classes concrètes implémentent les détails.
- Le code qui travaille avec l’abstraction ne dépend pas du type d’objet, ce qui rend le système flexible et extensible.
Tableau comparatif des approches
| Sans abstraction (if-else) | Avec abstraction (POO) |
|---|---|
| Beaucoup de conditions selon le type | Nouveau type — on modifie le code |
| Duplication de la logique | Logique — en un seul endroit |
| Difficile à étendre | Ajout — facile |
| Difficile à tester | Remplacer les implémentations — facile |
6. Erreurs typiques lors de la conception d’abstractions
Erreur n° 1 : L’abstraction pour l’abstraction.
Si vous n’avez qu’un seul type d’objet et qu’aucune extension n’est prévue — une classe abstraite n’est pas nécessaire. Inutile de compliquer le code sans raison.
Erreur n° 2 : Une abstraction trop générale.
Si la classe de base est trop « floue », les sous-classes peuvent n’avoir rien en commun hormis le nom. Par exemple, une abstraction « Thing » pour tout et n’importe quoi. Cela complique la maintenance et la compréhension du code.
Erreur n° 3 : Duplication de code dans les sous-classes.
Si tous les sous-classes ont la même implémentation d’une méthode, il convient de l’extraire dans la classe de base (la rendre non abstraite).
Erreur n° 4 : Violation du principe « du général au particulier ».
Si des détails qui ne concernent qu’une seule sous-classe apparaissent dans la classe abstraite — c’est que l’abstraction est mal choisie.
Erreur n° 5 : Oubli d’implémenter les méthodes abstraites.
Si vous n’implémentez pas toutes les méthodes abstraites dans la sous-classe, le compilateur vous obligera à rendre la classe abstraite elle aussi. Parfois, c’est inattendu :)
GO TO FULL VERSION