Vous observez une discussion animée entre Jenny et Marc. En effet, l’équipe qui définit les besoins fonctionnels souhaite apporter une modification à la façon de définir un bloc. Marc aurait aimé le savoir dès le début mais pas de panique, grâce au paradigme objet on va réussir à s’adapter.
Vous recevez une définition complétée de la notion de bloc :
Un bloc de construction est un élément reproductible par l’utilisateur. Le bloc est défini par 3 caractéristiques : longueur, largeur et hauteur.
Il peut exister plusieurs types de bloc de construction avec des caractéristiques propres à chaque type.
Voici les deux premiers types de bloc envisagés :
Mur : il a une caractéristique particulière, s’il est porteur alors on ne peut pas le franchir si non, c’est possible.
Porte : elle a une caractéristique particulière, si elle est verrouillée, il faut une clé pour la franchir.
Marc passe vous voir :
Ne t'inquiète pas, l’héritage en programmation orientée objet va nous permettre de faire le nécessaire.
La modélisation du résultat à obtenir est la suivante :
Comprenez l’héritage en programmation orientée objet
Les objets sont une solution efficace pour structurer le code des applications pour qu’il soit maintenable et évolutif.
Reprenons l’application Epicraft’s Journey. L’application gère des blocs et ces derniers se définissent par une hauteur, une largeur et une longueur. Nous avons donc créé une classe Bloc avec ces attributs.
Mais voilà que notre application évolue, elle doit désormais gérer également des blocs spécifiques que sont les murs. Et un mur se définit par les 3 mêmes caractéristiques (longueur, largeur et hauteur) auxquelles nous ajoutons une nouvelle caractéristique nommée porteur qui est un booléen. Un développeur devrait aller créer une nouvelle classe Mur avec 4 attributs !
Mais pourquoi ne pas modifier la classe Bloc et ajouter l’attribut porteur ?
Parce que pour notre application, un bloc n’est pas forcément un mur bien qu’un mur soit un bloc. Cependant il y a un point commun entre Bloc et Mur à savoir les trois attributs longueur, largeur et hauteur. Et ce point commun devient une duplication de code.
La programmation orientée objet nous permet de résoudre ce problème grâce à l’héritage. L’héritage est un concept qui permet à une classe dite “fille” de tirer profit d’une autre classe dite “mère”.
La classe fille peut accéder aux attributs et aux méthodes de la classe mère.
Observez le code suivant :
Bloc.java :
package ej;
public class Bloc {
protected int longueur;
protected int largeur;
protected int hauteur;
}
Mur.java
package ej;
public class Mur extends Bloc {
private boolean porteur;
}
À noter que la classe Mur utilise le mot-clé extends. On dit qu’elle étend la classe Bloc ou hérite de cette classe. Autrement dit, elle en devient une extension ou bien une spécialisation.
En étendant la classe Bloc, Mur accède désormais à ses attributs. Et cela ne vous aura pas échappé : la visibilité de longueur, largeur et hauteur est ni public, ni private mais protected. La visibilité protected rend accessible les attributs et méthodes aux classes filles en plus de la classe en elle-même. C’est donc un peu plus permissif que private mais pas autant que public.
Voici d’autres informations importantes :
Le mot-clé super permet d’accéder aux éléments de la classe mère à partir d’une classe fille.
Toutes les classes héritent automatiquement et de façon transparente d’une classe nommée Object. Nous aurons l’occasion d’en reparler, en attendant voici le lien vers sa documentation. C’est pour cela qu’en Java, on dit que tout est Object!
Pour empêcher une classe d’être étendue on peut utiliser le mot-clé final :
public final class MaClasseNonHeritable { }
Est-ce possible d’avoir un exemple concernant l’utilisation du mot-clé super ?
Bien sûr ! Reprenons le code précédent mais de façon plus complète :
Bloc.java :
package ej;
public class Bloc {
protected int longueur;
protected int largeur;
protected int hauteur;
public Bloc(final int longueur, final int largeur, final int hauteur) {
this.longueur = longueur;
this.largeur = largeur;
this.hauteur = hauteur;
}
}
Mur.java
package ej;
public class Mur extends Bloc {
private boolean porteur;
public Mur(final int longueur, final int largeur, final int hauteur, final boolean porteur) {
super(longueur, largeur, hauteur); // appel du constructeur de la classe mère
this.porteur = porteur;
}
public void afficherBloc() {
System.out.println(
super.longueur + “ “ +
super.largeur + “ “ +
super.hauteur + “ “ +
this.porteur);
}
}
Main.java
package ej;
public class Main {
public static void main(String[] args) {
Mur unBlocMur = new Mur(10,10,5,true);
unBlocMur.afficherBloc();
}
}
Ce code est :
évolutif car je peux ajouter une autre classe fille à Bloc sans impacter Mur ou Bloc
maintenable car je peux modifier Bloc et toutes les classes bénéficieront automatiquement de la modification sans duplication de code.
C’est déjà pas mal mais ce n’est pas tout ! Il faut à tout prix qu’on parle du polymorphisme
Optimisez vos développements avec le polymorphisme
Le mot polymorphisme vient de polymorphe qui est un adjectif qui signifie que l’élément polymorphe peut se présenter sous des formes différentes.
Dans le contexte de la programmation orientée objet, cela signifie qu’un objet peut se présenter sous des comportements différents.
Expliquons à travers la démonstration suivante :
Que pouvons-nous retenir ?
Une instance d’une classe fille peut être affectée à une variable typée par la classe mère :
Bloc unBloc = new Mur(10,10,5,true);
La redéfinition d’une méthode signifie qu’une classe fille implémente la même méthode qu’une classe mère (donc la même signature) mais en modifiant son comportement (donc des instructions différentes).
Même si la variable qui contient l’instance d’une classe fille est typée par la classe mère, lorsqu’une méthode est redéfinie, c’est le code de la classe fille qui est exécuté.
Le code est le suivant :
Bloc.java :
package ej;
public class Bloc {
protected int longueur;
protected int largeur;
protected int hauteur;
public Bloc(final int longueur, final int largeur, final int hauteur) {
this.longueur = longueur;
this.largeur = largeur;
this.hauteur = hauteur;
}
public void afficheUneDescription() {
System.out.println(“Je suis un bloc !”);
}
}
Mur.java
package ej;
public class Mur extends Bloc {
private boolean porteur;
public Mur(final int longueur, final int largeur, final int hauteur, final boolean porteur) {
super(longueur, largeur, hauteur);
this.porteur = porteur;
}
@Override
public void afficheUneDescription() {
System.out.println(“Je suis un mur !”);
}
}
Main.java
package ej;
public class Main {
public static void main(String[] args) {
Bloc unBloc = new Mur(10,10,5,true); // La variable est typé Bloc mais l’instance est bien Mur.
unBloc.afficherBloc(); // cette instruction affiche dans la console : Je suis un mur !
}
}
La redéfinition de la méthode est du polymorphisme à l’exécution car c’est uniquement lorsque le code sera exécuté que la méthode à exécuter sera identifiée. Ce polymorphisme existe grâce à l’héritage !
Il existe également du polymorphisme à la compilation que l’on appelle surcharge. Dans ce cas, nul besoin d’héritage.
Une classe peut avoir plusieurs méthodes de même nom mais avec des paramètres différents. Étant donné que ces méthodes ont le même nom, on pourrait croire qu’il y a des doublons mais en fait ce sont des signatures différentes.
Voici un exemple :
Mur.java
package ej;
public class Mur extends Bloc {
private boolean porteur;
public Mur(final int longueur, final int largeur, final int hauteur, final boolean porteur) {
super(longueur, largeur, hauteur);
this.porteur = porteur;
}
public void afficheUneDescription(final String description) {
System.out.println(description);
}
public void afficheUneDescription() {
this.afficheUneDescription(“Je suis un mur !”);
}
}
Il y deux méthodes qui ont pour nom afficheUneDescription, une n’a pas de paramètre et l’autre en a un. L’une surcharge l’autre.
Implémentez l’abstraction de classes et de méthodes
On a presque fini ce chapitre, il nous reste à voir la notion d’abstraction. Ce concept peut s’appliquer au niveau de la classe ou au niveau d’une méthode. Dans le deux cas, c’est directement lié à l’héritage.
Au niveau de la classe, voici la définition : Une classe abstraite ne peut pas être instanciée.
Mais comment fait-on pour s’en servir alors ?
Rassurez-vous, une classe abstraite est faite pour être héritée ! Il existe de nombreuses situations où l’on veut bloquer l’instanciation. Par exemple, imaginez 3 classes : Animal, Chien et Chat. Bien évidemment Chien et Chat sont deux classes filles de la classe Animal. Dans ce contexte, un Animal peut-il exister sans être un chien ou un chat ? Absolument pas, c’est un concept abstrait qui sert à regrouper deux éléments concrets Chien et Chat.
Voici un code qui montre comment rendre abstraite une classe :
Animal.java
public abstract class Animal {
}
Le mot clé abstract se place donc avant le mot clé class.
Au niveau de la méthode, la définition est la suivante : Une méthode abstraite ne peut pas être implémentée dans la classe où elle est définie et devra obligatoirement être implémentée dans la classe fille.
Cela revient à forcer la redéfinition ! Notez le code suivant :
Animal.java
public abstract class Animal {
public abstract void cri() { }
}
Chien.java
public class Chien extends Animal {
public void cri() {
System.out.println(“Wouaf”);
}
}
Chat.java
public class Chat extends Animal {
public void cri() {
System.out.println(“Miaou”);
}
}
Dans cet exemple, la classe mère abstraite ne possède qu’une méthode abstraite mais soyez conscient qu’elle aurait pu posséder d’autres méthodes, abstraites ou non.
Sur ce dernier point, mon exemple n’utilisait pas le projet Epicraft’s Journey, mais rassurez-vous, dans la section suivante nous allons pratiquer l’abstraction dans le contexte de notre jeu !
À vous de jouer
Contexte
A la suite des nouvelles informations transmises à Marc par Jenny, de nouvelles tâches vous sont affectées :
ID de la tâche : 3
Nom de la tâche : Modifier l’objet Bloc
Description de la tâche :
L’objet Bloc sera étendu, il ne doit donc plus être instanciable directement. Ces attributs doivent également être disponibles pour les classes filles qui seront implémentées ultérieurement.
et
ID de la tâche : 4
Nom de la tâche : Créer la classe Mur.
Description de la tâche :
Respecter la définition métier de ces éléments :
- Mur : il a une caractéristique particulière, s’il est porteur alors on ne peut pas le franchir si non, c’est possible.
et
ID de la tâche : 5
Nom de la tâche : Créer la classe Porte.
Description de la tâche :
Respecter la définition métier de ces éléments :
- Porte : elle a une caractéristique particulière, si elle est verrouillée, il faut une clé pour la franchir.
Consignes
1- Commencez par modifier la classe Bloc en utilisant l’abstraction au niveau de la classe et la visibilité protected pour les attributs.
2- Créez une nouvelle classe Mur et ajouter un attribut booléen pour la caractéristique porteur. Mur est une classe fille de Bloc.
La classe Mur doit avoir un constructeur paramétré permettant de définir toutes les caractéristiques et faisant appel également au constructeur de la classe mère.
La classe Mur doit avoir une méthode de nom estTraversable sans paramètres avec le type retour boolean. Si la caractéristique porteur est vraie, alors la méthode estTraversable doit renvoyer faux, sinon elle doit renvoyer vrai.
3- Créez une nouvelle classe Porte et ajouter un attribut booléen pour la caractéristique verrouille. Porte est une classe fille de Bloc.
La classe Porte doit avoir un constructeur paramétré permettant de définir toutes les caractéristiques et faisant appel également au constructeur de la classe mère.
La classe Porte doit avoir une méthode de nom estVerrouilee sans paramètres avec le type retour boolean. Si la caractéristique verrouille est vraie, alors la méthode estVerrouille doit renvoyer vrai, sinon elle doit renvoyer faux.
En résumé
L’héritage est fondamental pour traiter des problèmes de maintenabilité et évolutivité.
Le mot clé extends permet à une classe dite fille d’être une extension d’une autre classe dite mère.
Une classe fille peut accéder aux attributs et méthodes de la classe mère qui ont au minimum une visibilité protected grâce à l’utilisation du mot clé super.
La redéfinition et la surcharge sont deux techniques de polymorphisme.
L’abstraction au niveau d’une classe empêche l’instanciation de cette dernière, l’abstraction au niveau d’une méthode force la redéfinition de cette dernière dans les classes filles.
Suivez-moi dans le prochain chapitre pour découvrir d’autres possibilités que nous offre Java et la programmation orientée objet !