1. Construire une hiérarchie : de l’abstraction aux détails
Implémenter des abstractions et des hiérarchies est une façon de structurer le code, des règles générales vers les détails concrets. D’abord, nous décrivons ce que tous les objets doivent savoir faire (abstraction), puis nous précisons comment chaque classe concrète le fait.
En programmation, comme dans la vie, tout commence par des questions. Par exemple : « Qu’ont en commun un cercle et un rectangle ? » Réponse : ce sont tous deux des formes. Et qu’ont en commun les formes ? En général, elles ont une surface et on peut les dessiner.
En Java, cela s’exprime via une classe abstract :
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
Ici, nous disons :
- Toute forme doit savoir calculer sa surface (area()).
- Toute forme doit savoir se dessiner (draw()).
- La façon exacte de le faire — ce n’est pas notre affaire (pour l’instant).
Créons maintenant des formes concrètes :
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Nous dessinons un cercle de rayon " + radius);
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("Nous dessinons un rectangle " + width + "x" + height);
}
}
Qu’avons-nous fait ?
- Nous avons extrait le commun dans une classe abstraite.
- Nous avons détaillé le comportement dans les sous-classes.
Schématiquement :
. Shape
/ \
Circle Rectangle
Tableau : ce qui est implémenté où
| Classe | area() | draw() | Champs propres |
|---|---|---|---|
| Shape | |
|
- |
| Circle | implémenté | implémenté | |
| Rectangle | implémenté | implémenté | |
2. Pourquoi est-ce pratique ? (et pourquoi cela fonctionne-t-il)
Interface unifiée pour manipuler des objets différents
Supposons que vous ayez une collection de formes :
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
Vous pouvez les parcourir de la même manière, sans penser au type :
for (Shape shape : shapes) {
shape.draw();
System.out.println("Surface : " + shape.area());
}
Laissez la JVM se débrouiller pour savoir qui est un cercle et qui est un rectangle ! C’est ça, le polymorphisme (nous en avons déjà parlé, et nous en reparlerons plus en détail — dans le prochain bloc).
Facilité d’extension
Vous voulez ajouter un triangle ? Il suffit d’écrire (nouveau type — on ne modifie pas l’ancien code) : Triangle extends Shape.
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("Nous dessinons un triangle : base " + base + ", hauteur " + height);
}
}
Le reste du code (par exemple, la boucle qui parcourt la liste des formes) n’a pas besoin d’être modifié.
Éviter la duplication de code
Si toutes les formes acquièrent une propriété commune (par exemple, la couleur), il est pratique de l’extraire dans la classe abstraite :
public abstract class Shape {
private String color = "noir";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
Ainsi, n’importe quel héritier — cercle ou triangle — héritera de la couleur « par défaut ».
3. Pratique : concevons un mini éditeur graphique
Essayons d’assembler tout cela. Imaginez que vous réalisiez un petit éditeur graphique.
Classe abstraite Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
Formes concrètes
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Nous dessinons une ligne de longueur " + length + " de couleur " + getColor());
}
@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("Nous dessinons une ellipse avec des axes " + a + " et " + b + " de couleur " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Nouveaux axes de l'ellipse : " + a + ", " + b);
}
}
Vous voulez ajouter un nouvel outil, par exemple Polygon ? Créez simplement une nouvelle classe — tout le code de l’éditeur fonctionne via l’abstraction Figure.
Utilisation dans le code
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
Sortie :
Nous dessinons une ligne de longueur 10.0 de couleur red
Nouvelle longueur de la ligne : 15.0
Nous dessinons une ellipse avec des axes 5.0 et 3.0 de couleur red
Nouveaux axes de l'ellipse : 7.5, 4.5
Visualisation de la hiérarchie
. Figure
/ \
Line Ellipse
4. Comment éviter la duplication : champs et méthodes communs
Parfois, tous les héritiers ont non seulement des méthodes communes, mais aussi des champs communs (par exemple, les coordonnées du centre). La classe abstraite est l’endroit idéal pour cela :
public abstract class Figure {
private double x, y; // coordonnées du centre
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("La figure a été déplacée au point (" + x + ", " + y + ")");
}
public abstract void draw();
}
Désormais, Line ou Ellipse peuvent se déplacer sans réimplémenter cette méthode.
5. Un autre exemple : systèmes de paiement
L’abstraction ne concerne pas que les formes ! Imaginez que vous écriviez un système de traitement des paiements.
Classe abstraite Payment
public abstract class Payment {
public abstract void process();
}
Implémentations concrètes
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("Traitement d'un paiement par carte bancaire");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("Traitement d'un paiement via PayPal");
}
}
Utilisation
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
Sortie :
Traitement d'un paiement par carte bancaire
Traitement d'un paiement via PayPal
6. Avantages de cette approche
- Interface unifiée : on peut travailler de la même façon avec des objets différents.
- Extensibilité : l’ajout de nouveaux types d’objets ne nécessite pas de réécrire l’ancien code.
- Minimum de duplication : le commun est extrait dans une classe abstraite de base.
- Flexibilité : on peut utiliser des collections de types abstraits sans se soucier des détails.
7. Exemple concret : transport
L’abstraction ne se rencontre pas que dans les manuels. Par exemple, si vous concevez un système de gestion du transport :
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
Les types de transport concrets réalisent les détails :
public class Car extends Transport {
@Override
public void move() {
System.out.println("La voiture roule sur la route");
}
@Override
public void fuelUp() {
System.out.println("Faire le plein d'essence");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("Le vélo avance en pédalant");
}
@Override
public void fuelUp() {
System.out.println("Le vélo n'a pas besoin de carburant, seulement de sandwiches pour le cycliste !");
}
}
8. Schéma utile : comment construire une hiérarchie d’abstractions
[Classe abstraite]
|
[Sous-classe concrète]
|
[Sous-classe encore plus concrète] (si nécessaire)
- Tout ce qui est commun — en haut !
- Tout ce qui est spécifique — en bas !
9. Erreurs typiques lors de l’implémentation d’abstractions et de hiérarchies
Erreur n° 1 : duplication de code dans les sous-classes.
Si vous remarquez que vous écrivez les mêmes champs ou méthodes dans chaque sous-classe, c’est le signal qu’il faut les extraire dans la classe abstraite. N’ayez pas peur de rendre l’abstraction « plus large » si cela réduit la duplication.
Erreur n° 2 : violation du principe « du général au particulier ».
Parfois, les débutants commencent à construire la hiérarchie « à partir des détails », en oubliant le commun. Il en résulte des classes étranges comme RedCircleWithShadow, qui s’intègrent mal dans la structure globale. Identifiez toujours d’abord l’abstraction, puis les détails.
Erreur n° 3 : hiérarchies trop profondes.
Si votre chaîne d’héritage dépasse 3–4 niveaux, demandez-vous s’il ne serait pas temps d’utiliser la composition ou des interfaces plutôt que l’héritage.
Erreur n° 4 : implémentation forcée de méthodes non pertinentes.
Si une classe abstraite contient trop de méthodes abstraites, non pertinentes pour certains héritiers, il est peut-être temps de revoir la structure. Par exemple, tous les moyens de transport n’ont pas besoin d’une méthode fuelUp() (elle ne sert à rien au vélo).
Erreur n° 5 : confusion entre classe abstraite et interface.
Classe abstraite — lorsqu’il existe un état commun et/ou une implémentation partielle. Interface — lorsqu’il faut seulement « promettre » la présence de méthodes, sans stocker de données ni implémenter de comportement. Ne mélangez pas ces approches sans nécessité.
GO TO FULL VERSION