Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Cet article présente la notion importante qu'est l'héritage en programmation orientée objets. Nous parlerons de l'héritage d'interface ainsi que de l'héritage d'implémentation.

Pré-requis

Cet article s'appuie sur un programme Java utilisant la bibliothèque graphique SWT. Une connaissance élémentaire de Java est indispensable. Quand à SWT, nous resterons très basique. Par conséquent, il vous suffira pour cet article d'avoir installé la bibliothèque sur votre système et de savoir compiler et exécuter un programme l'utilisant.

Programme d'exemple

Le programme qui sert de support à cet article est un simple outil de dessin. Celui-ci affiche dans une fenêtre un certain nombre d'éléments graphiques prédéfinis. Le programme n'est pas intéractif.

Nous allons imaginer que ce logiciel est utilisé par un paysagiste afin de visualiser l'implantation de différents éléments dans un jardin. Les éléments à afficher seront donc divers:

Version 0

Pour commencer, nous allons immédiatement donner le programme principal:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
 
public class Jardinou {
	public Jardinou(Plan plan) {
		display	= new Display();
		shell	= new Shell(display);
 
		Canvas	canvas	= new Canvas(shell, SWT.NONE);
		canvas.setSize(400, 400);
 
		// rend le 'plan' responsable de l'affichage
		canvas.addPaintListener(plan);
	}
 
	public void affiche() {
		shell.pack();
		shell.open();
 
		while(!shell.isDisposed()) {
			if (!display.readAndDispatch())
				display.sleep();
		}
 
		display.dispose();
	}
 
	private Display display;
	private Shell	shell;
 
 
	public static void main(String[] args) {
		Plan		plan 		= new Plan();
		Jardinou	jardinou	= new Jardinou(plan);
 
		jardinou.affiche();
	}
}

Si vous ne connaissez pas bien SWT, le code peut vous sembler un peu obscur. Pour cet article, la seule chose à savoir est qu'un canvas (une instance de la classe org.eclipse.swt.widgets.Canvas) est un widget dans lequel il est possible de dessiner (en clair, c'est une zone de dessin...).

Vous remarquerez aussi le commentaire dans le code qui indique que le plan est responsable du dessin dans le canvas. Le plan est instancié dans le programme principal. En voici le code:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.GC;
 
 
public class Plan implements PaintListener {
	public Plan() {
		// initialisation
	}
 
	@Override
	public void paintControl(PaintEvent e) {
		// Le code de dessin prend place ici:
		GC gc = e.gc;
		gc.drawLine(0, 0, 400, 400);
		gc.drawLine(0, 400, 400, 0);
		gc.drawRectangle(20, 20, 360, 360);
	}
}

La méthode importante dans notre plan est paintControl c'est la méthode chargée du dessin à proprement parler. A titre d'exemple, j'ai inclus dans le code ci-dessus le code nécessaire pour dessiner des lignes et un Bâtiment. Mais la classe org.eclipse.swt.graphics.GC propose bien d'autres méthodes de dessin!

Remarque:

D'ici la fin de cet article, vous saurez la signification du mot-clé implements et de l'annotation @Override.

Compiler

Avant de poursuivre, assurez-vous d'avoir bien les deux fichiers ci-dessus au bon endroit, et vérifiez que vous pouvez compiler et exécuter le programme.

sh$ find .
.
./fr
./fr/chicoree
./fr/chicoree/jardinou
./fr/chicoree/jardinou/Jardinou.java
./fr/chicoree/jardinou/Plan.java
sh$ javac -cp .:/usr/local/lib/swt/swt.jar fr/chicoree/jardinou/*.java
sh$ java -cp .:/usr/local/lib/swt/swt.jar fr.chicoree.jardinou.Jardinou

Version 0.1

Maintenant que nous avons les fondations de notre programme, il est temps de mettre en place les différents éléments à dessiner.

A priori, le programme peut dessiner un nombre arbitraire d'éléments. Ceux-ci vont donc devoir être mémorisés dans une liste. De la même manière, il faut permettre d'ajouter des éléments dans la liste. Nous aurons donc besoin de fournir une méthode pour cela.

Pour cette version, nous nous contenterons d'afficher des blocs rectangulaires.

Bref, le code modifié de la classe fr.chicoree.jardinou.Plan va devenir:

package fr.chicoree.jardinou;
 
import java.util.ArrayList;
import java.util.List;
 
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.GC;
 
 
public class Plan implements PaintListener {
	public Plan() {
		// initialisation
		elements = new ArrayList<Batiment>();
	}
 
	public void ajoute(Batiment r) {
		elements.add(r);
	}
 
	@Override
	public void paintControl(PaintEvent e) {
		// Le code de dessin prend place ici:
		GC gc = e.gc;
		for(Batiment element : elements) {
			element.dessine(gc);
		}
	}
 
	private List<Batiment>	elements;
}

Reste le code de la classe fr.chicoree.jardinou.Batiment. Dans notre programme, un Bâtiment est défini par ses 4 coins. Et il est représenté par un rectangle avec des segments reliant les coins opposés. Ce qui donne le code suivant:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public class Batiment {
	public Batiment(int x1, int y1, int x2, int y2) {
		this.x1 = x1;
		this.x2 = x2;
		this.y1 = y1;
		this.y2 = y2;
	}
 
	public void dessine(GC gc) {
		gc.drawRectangle(x1, y1, x2-x1, y2-y1);			
		gc.drawLine(x1, y1, x2, y2);
		gc.drawLine(x1, y2, x2, y1);
	}
 
	private int	x1;
	private int	x2;
	private int	y1;
	private int	y2;
}

Remarque:

drawLine et drawRectangle n'utilisent pas les mêmes paramètres

drawLine utilise les coordonnées des extrémités du segment pour le dessiner:

gc.drawLine(x1, y1, x2, y2);

Alors que drawRectangle utilise les coordonnées du coin suppérieur-gauche ainsi que la largeur et la longueur du bâtiment:

gc.drawRectangle(x1, y1, x2-x1, y2-y1);

Bien sûr, vous pouvez recompiler et exécuter le programme. Et vous constaterez que ... plus rien ne s'affiche! Effectivement, nous avons oublié d'ajouter les bâtiments à afficher dans le plan. Il faut donc modifier le programme principal:

/* ... */
	public static void main(String[] args) {
		Plan		plan 		= new Plan();
		plan.ajoute(new Batiment(20, 20, 80, 200));
		plan.ajoute(new Batiment(120, 80, 160, 120));
		plan.ajoute(new Batiment(220, 220, 360, 360));
 
		Jardinou	jardinou	= new Jardinou(plan);
 
		jardinou.affiche();
	}

Ajouter d'autres éléments

Le programme précédent fonctionne très bien tant que nous n'avons que des blocs rectangulaires figurant les bâtiments. Mais que se passerait-il si nous voulions dessiner par exemple des arbres?

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public class Arbre {
	public Arbre(int x, int y, int r) {
		this.x = x;
		this.y = y;
		this.r = r;
	}
 
	public void dessine(GC gc) {
		gc.drawOval(x-r, y-r, r*2, r*2);		
	}
 
	private int	x;
	private int	y;
	private int	r;
}

Comme vous le voyez dans le code ci-dessus, un arbre est figuré dans notre programme par un cercle. Mais comment ajouter un arbre à notre plan?

La première idée qui peut venir à l'esprit est de programmer par copier-coller – en reproduisant le code utilisé pour les bâtiments, mais cette fois pour les arbres. Par conséquent, il faudrait modifier la classe fr.chicoree.jardinou.Plan ainsi:

public class Plan implements PaintListener {
	public Plan() {
		// initialisation
		elements = new ArrayList<Batiment>();
		arbres = new ArrayList<Arbre>();
	}
 
	public void ajoute(Batiment r) {
		elements.add(r);
	}
 
	public void ajoute(Arbre a) {
		arbres.add(a);
	}
 
	@Override
	public void paintControl(PaintEvent e) {
		// Le code de dessin prend place ici:
		GC gc = e.gc;
		for(Batiment element : elements) {
			element.dessine(gc);
		}
		for(Arbre arbre : arbres) {
			arbre.dessine(gc);
		}
	}
 
	private List<Batiment>	elements;
	private List<Arbre>		arbres;
}

Et quand au programme principal, il serait alors possible d'ajouter des arbres à notre jardin:

/* ... */
	public static void main(String[] args) {
		Plan		plan 		= new Plan();
		plan.ajoute(new Batiment(20, 20, 80, 200));
		plan.ajoute(new Batiment(120, 80, 160, 120));
		plan.ajoute(new Batiment(220, 220, 360, 360));
 
		plan.ajoute(new Arbre(250, 60, 50));
		plan.ajoute(new Arbre(49, 320, 10));
		plan.ajoute(new Arbre(71, 320, 10));
		plan.ajoute(new Arbre(60, 301, 10));
 
		Jardinou	jardinou	= new Jardinou(plan);
 
		jardinou.affiche();
	}

Héritage d'interface

Bien sûr, la solution exposée plus haut fonctionne. Mais elle n'est pas très élégante: d'une part, il y a déjà une importante duplication de code, et d'autre part, à chaque fois que nous voudrions ajouter un nouvel élément de dessin, il faudrait encore dupliquer le même code.

Heureusement, il y a moyen de faire mieux. En effet, si l'on observe les classes fr.chicoree.jardinou.Batiment et fr.chicoree.jardinou.Arbre, on peut remarquer qu'elles proposent toutes deux la méthode:

	public void dessine(GC gc) {
                /* ... */
	}

Bien sûr dans les deux cas, le travail réalisé par la méthode est différent. En attendant, de l'extérieur, ces deux méthodes s'utilisent de la même manière. Ce qui est visible dans le code de la classe fr.chicoree.jardinou.Plan:

		for(Batiment element : elements) {
			element.dessine(gc);
		}
		for(Arbre arbre : arbres) {
			arbre.dessine(gc);
		}

En fait, fr.chicoree.jardinou.Batiment et fr.chicoree.jardinou.Arbre offrent toutes deux la même interface en ce qui concerne le dessin. Java permet de formaliser cela au niveau du langage avec la construction suivante:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public interface Dessinable {
 
	public abstract void dessine(GC gc);
 
}

Remarque:

L'interface va dans son propre fichier source (ici fr/chicoree/jardinou/Dessinable.java), exactement comme pour une classe.

Note:

Le mot-clé abstract signifie que la méthode est déclarée mais pas définie.

Par définition une interface ne fait que déclarer des méthodes. Elle n'en définit jamais. Par conséquent le mot-clé abstract est optionnel dans la déclaration d'une interface.

Il faut aussi explicitement indiquer quelles classes se conforment à notre interface. Cela se fait à l'aide du mot-clé implements dans la déclaration des classes:

public class Batiment implements Dessinable {
/* ... */
}
public class Arbre implements Dessinable {
/* ... */
}

En vocabulaire de programmation orienté objets, on dit que Arbre et Bâtiment héritent de l'interface Dessinable.

Note:

Toutes les classes qui implémentent l'interface fr.chicoree.jardinou.Dessinable possèdent une méthode dessin. Par contre chaque classe possède sa propre mise en oeuvre de cette méthode (tantôt pour dessiner des Bâtiments, tantôt des cercles).

On dit que la méthode est redéfinie (override en anglais). En Java la redéfinition est implicite: il suffit d'avoir une méthode de même nom et avec les mêmes paramètres que dans l'interface. Néanmoins, depuis Java 5, on considère de plus en plus que c'est du bon style que d'indiquer explicitement à l'aide de l'annotation @Override:

/* ... */
	@Override
	public void dessine(GC gc) {
		gc.drawLine(x1, y1, x2, y2);
		gc.drawLine(x1, y2, x2, y1);
		gc.drawRectangle(x1, y1, x2-x1, y2-y1);		
	}

L'annotation @Override, pour toute facultative qu'elle soit, a au moins deux avantages:

  • elle a un rôle documentaire en indiquant à un programmeur lisant le code source qu'une méthode fournit une nouvelle mise en oeuvre;
  • elle permet au compilateur de vérifier que le nom et les arguments de la méthode correspondent bien à ceux d'une méthode héritée.

Et à quoi tout cela nous avance? Et bien maintenant, nous pouvons manipuler de manière uniforme tous les objets qui mettent en oeuvre l'interface Dessinable. Que ce soient des instances de la classe Batiment, Arbre ou quoi que ce soit d'autre.

Ainsi, la code de la classe fr.chicoree.jardinou.Plan se simplifie:

package fr.chicoree.jardinou;
 
import java.util.ArrayList;
import java.util.List;
 
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.GC;
 
 
public class Plan implements PaintListener {
	public Plan() {
		// initialisation
		elements = new ArrayList<Dessinable>();
	}
 
	public void ajoute(Dessinable r) {
		elements.add(r);
	}
 
	@Override
	public void paintControl(PaintEvent e) {
		// Le code de dessin prend place ici:
		GC gc = e.gc;
		for(Dessinable element : elements) {
			element.dessine(gc);
		}
	}
 
	private List<Dessinable>	elements;
}

Et c'est tout: maintenant, notre plan sait dessiner n'importe quel élément qui implémente l'interface fr.chicoree.jardinou.Dessinable!

Remarque:

Le programme principal ne change pas:

/* ... */
		plan.ajoute(new Batiment(20, 20, 80, 200));
		plan.ajoute(new Batiment(120, 80, 160, 120));
		plan.ajoute(new Batiment(220, 220, 360, 360));
 
		plan.ajoute(new Arbre(250, 60, 50));
		plan.ajoute(new Arbre(49, 320, 10));
		plan.ajoute(new Arbre(71, 320, 10));
		plan.ajoute(new Arbre(60, 301, 10));

Mais maintenant, tous les appels à la méthode ajoute font appel à la même méthode void ajoute(Dessinable r).

A vous de jouer

On souhaite pouvoir ajouter des repères sur le plan. Les repères sont indiqués par une croix et un texte descriptif est affiché à côté.

Ecrivez la classe fr.chicoree.jardinou.Repere pour qu'on puisse l'utiliser ainsi dans le programme principal:

/* ... */
		plan.ajoute(new Repere(40, 40, "Accès EDF"));

Vous aurez sans doute besoin de la méthode org.eclipse.swt.graphics.GC.drawString

Héritage d'implémentation

Maintenant, nous souhaitons ajouter un nouveau type d'élément à notre programme. Il s'agit des "bâtiments temporaires" (comme des constructions saisonnières). Ceux-ci sont représentés comme des bâtiments normaux, mais en pointillés. Naturellement, de ce que nous avons vu précédemment, il vient:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public class BatimentTemporaire implements Dessinable {
	public BatimentTemporaire(int x1, int y1, int x2, int y2) {
		this.x1 = x1;
		this.x2 = x2;
		this.y1 = y1;
		this.y2 = y2;
	}
 
	@Override
	public void dessine(GC gc) {
		gc.setLineDash(new int[] {1, 2, 4, 2});
		gc.drawLine(x1, y1, x2, y2);
		gc.drawLine(x1, y2, x2, y1);
		gc.drawRectangle(x1, y1, x2-x1, y2-y1);
		gc.setLineDash(null);
	}
 
	private int	x1;
	private int	x2;
	private int	y1;
	private int	y2;
}

A nouveau, c'est très bien et ça fonctionne. A ceci près que ce code est quasiment le même que celui de la classe fr.chicoree.jardinou.Batiment. Le seul changement significatif se situe sur les lignes qui passent en mode pointillé – puis reviennent au mode d'affichage par défaut:

	@Override
	public void dessine(GC gc) {
		gc.setLineDash(new int[] {1, 2, 4, 2});
		gc.drawLine(x1, y1, x2, y2);
		gc.drawLine(x1, y2, x2, y1);
		gc.drawRectangle(x1, y1, x2-x1, y2-y1);
		gc.setLineDash(null);
	}

Ce qui serait bien, ce serait de pouvoir dire "un bâtiment temporaire se comporte exactement comme un bâtiment, sauf qu'il est dessiné en pointillés". C'est exactement ce que permet l'héritage d'implémentation: on récupère la mise en oeuvre d'une classe et on ne redéfinit que les méthodes à changer. Pour indiquer cela à Java, on utilise le mot-clé extends:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public class BatimentTemporaire  extends Batiment {
/* ... */
}

Quand à la méthode dessine, elle doit:

  1. Passer en mode pointillé;
  2. Dessiner le bâtiment;
  3. Repasser en mode trait plein.

Les point 1 et 3 donnent:

/* ... */
	@Override
	public void dessine(GC gc) {
		gc.setLineDash(new int[] {1, 2, 4, 2});
 
		// ici ajouter le dessin du bâtiment
 
		gc.setLineDash(null);
	}

Remarquez aussi que "dessiner le bâtiment" c'est déjà ce que faisait la méthode dessine de la classe Batiment. Java nous permet d'appeler l'implémentation de base (celle définie dans la classe dont on hérite) avec le mot-clé super. Au final, le code complet de la méthode dessine devient:

/* ... */
	@Override
	public void dessine(GC gc) {
		gc.setLineDash(new int[] {1, 2, 4, 2});
 
		super.dessine(gc);
 
		gc.setLineDash(null);
	}

Si vous essayez d'utiliser la classe BatimentTemporaire vous constaterez que ça n'est pas possible (en fait, la classe ne compile même pas). En effet, l'usage typique serait:

/* ... */
		plan.ajoute(new BatimentTemporaire(220, 180, 360, 220));

Or la ligne de code précédente crée bien un objet – ce qui a pour conséquence d'invoquer son constructeur, ici avec pour argument les coordonnées du bâtiment. Mais notre classe BatimentTemporaire n'a tout simplement pas de constructeur. Il faut donc lui en ajouter un.

/* ... */
	public BatimentTemporaire(int x1, int y1, int x2, int y2) {
		// initialisation
	}

Ici, BatimentTemporaire n'a rien de spécial à faire. Si ce n'est passer au constructeur de sa classe de base les coordonnées du bâtiment. A nouveau, le mot-clé super est utilisé, mais dans une syntaxe légèrement différente:

/* ... */
	public BatimentTemporaire(int x1, int y1, int x2, int y2) {
		super(x1, y1, x2, y2);
	}

Au final, voici le code complet de BatimentTemporaire:

package fr.chicoree.jardinou;
 
import org.eclipse.swt.graphics.GC;
 
public class BatimentTemporaire extends Batiment {
	public BatimentTemporaire(int x1, int y1, int x2, int y2) {
		super(x1, y1, x2, y2);
	}
 
	@Override
	public void dessine(GC gc) {
		gc.setLineDash(new int[] {1, 2, 4, 2});
 
		super.dessine(gc);
 
		gc.setLineDash(null);
	}
}

Chacun ses affaires!

Les méthodes de la classe fr.chicoree.jardinou.BatimentTemporaire ne peuvent pas accéder aux données membres de la classe de base si elles sont déclarées private (c'est le cas ici pour les coordonnées du bâtiment).

Si on voulait donner accès à ces données à la classe BatimentTemporaire, il faudrait les déclarer protected:

public class Batiment implements Dessinable {
/* ... */
	protected int	x1;
	protected int	x2;
	protected int	y1;
	protected int	y2;
}

A vous de jouer

  1. On souhaite distinguer les arbres caducs des arbres persistants. A cette fin, on veut ajouter la classe fr.chicoree.jardinou.ArbreCaduc qui se comportera comme fr.chicoree.jardinou.Arbre à ceci près que l'arbre est dessiné en pointillés (4 noirs, 2 blancs, 4 noirs, 2 blancs, 4 noirs, 2 blancs, etc.).
  2. En plus d'être dessinés en pointillés, on souhaite que le mot "(caduc)" apparaisse au centre de l'arbre (vous aurez peut-être besoin d'accéder aux coordonnées de l'arbre déclarées dans fr.chicoree.jardinou.Arbre: si c'est le cas, vous devrez changer leur visibilité de private à protected).