Une nouvelle évolution est requise pour le projet Epicrafter’Journey. Marc étant parti en vacances, Jenny vient directement vous voir !
Il faudrait pouvoir associer une couleur à tous les blocs. Mais la liste des couleurs est prédéfinie. Pour l’instant on souhaite juste du bleu, du vert, du gris, du marron et du noir.
Avant ses vacances, Marc vous a demandé d’approfondir votre connaissance de la programmation orientée objet en abordant trois nouveaux thèmes : les interfaces, les records et les énumérations. Avant de vous lancer dans la tâche que Jenny vous a demandé, vous décidez de vous y plonger pour voir si quelque chose pourrait vous aider.
Le résultat à obtenir à la fin du chapitre correspond à la modélisation suivante :
Appliquez un contrat à vos classes en utilisant des interfaces
La programmation orientée objet offre la capacité aux développeurs de structurer le code. Cette structure signifie parfois imposer des contraintes. Par exemple, la visibilité privée est une contrainte qui empêche d’accéder à un attribut ou à une méthode en dehors de la classe. Autre exemple, l’abstraction est une contrainte qui empêche l’instanciation d’une classe.
D’ailleurs n’est-ce pas un inconvénient de devoir composer avec tant de contraintes ?
On pourrait le croire, mais en fait c’est tout l’inverse. Lorsqu’on construit une maison, on est contraint d’avoir des murs porteurs, c’est-à-dire des murs qui pourront supporter toute la structure de la maison. S’il est vrai que c’est une contrainte car on ne peut pas faire sans, les habitants de la maison seront plus que ravis de savoir qu’il y a des murs porteurs !
De la même façon les contraintes imposées en programmation orientée objet vont renforcer la maintenabilité et l’évolutivité du code et donc finalement la qualité de l’application.
Personnellement en tant qu’architecte logiciel, je préfère un projet développé en Java qui impose plus de rigueurs aux développeurs, que l’utilisation des langages de programmation permissifs qui malheureusement laissent beaucoup de plus de marges d’erreurs.
J’aimerais donc maintenant vous présenter un nouvel élément disponible en Java qui permet une nouvelle fois de contraindre le développeur grâce à la démonstration suivante :
Nous pouvons retenir les points suivants :
Les interfaces ressemblent à des classes mais n’en sont pas réellement. Le mot-clé interface est utilisé.
Une interface peut avoir des variables qui sont automatiquement public, static et final, on dit que ce sont des constantes.
Une interface peut avoir des signatures de méthodes mais pas l’implémentation de ces méthodes.
Une classe peut tirer profit d’une interface en l’implémentant grâce au mot-clé implements.
Une variable peut être typée par une interface.
Le code de cette démonstration est lié au projet Epicraft’s Journey, nous avons créé une interface nommée IBloc qui possède trois constantes et qui doit contraindre les blocs à implémenter une méthode qui a la signature public void afficherDescription();
.
IBloc.java
package ej;
public interface IBloc {
int MIN_LONGUEUR = 1;
int MIN_LARGEUR = 1;
int MIN_HAUTEUR = 1;
public void afficherDescription();
}
Bloc.java
package ej;
public abstract class Bloc implements IBloc { // implémentation de l’interface
protected int longueur;
protected int largeur;
protected int hauteur;
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 { // par extension, Mur va devoir respecter l’interface
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 boolean isTraversable() {
return !porteur;
}
@Override
public void afficherDescription() {
System.out.println("Je suis un mur !");
}
}
À noter que la classe Bloc étant abstract, l’interface ne peut lui contraindre à implémenter la méthode afficherDescription. Ce sont donc les classes filles de Bloc qui seront contraintes.
Je trouve également intéressant de noter qu’une interface a le même comportement qu’une classe abstraite qui contient uniquement des méthodes abstraites !
On compare souvent une interface à un contrat qu’une classe doit respecter. Toutes les classes qui implémentent la même interface respectent donc le même contrat.
Dans quelle situation cela peut être utile ?
Très bonne question ! Bien sûr il y a plusieurs cas, mais voyons une problématique intéressante : le couplage entre les classes.
Réduisez le couplage de vos classes grâce à l’injection de dépendances
Commençons par définir ce qu’est le couplage. Prenons un exemple, dans une maison un évier ne fonctionne pas s’il n’est pas raccordé à l’arrivée d’eau. On peut dire que l’évier utilise l’arrivée d’eau ou bien qu’il dépend de l’arrivée d’eau.
De la même façon, une classe peut utiliser une autre classe et elle dépendra donc de cette classe. Sans cette dernière, elle ne fonctionnerait pas. On dit alors que ces deux classes sont couplées car l’une a besoin de l’autre, ou encore car l’une dépend de l’autre.
Le couplage est donc le résultat d’une dépendance (ou d’un lien) entre deux classes.
Pourquoi est-ce une problématique ?
Reprenons notre exemple, vu que l’évier dépend de l’arrivée d’eau, si vous déplacez l’arrivée d’eau à plusieurs mètres de l’évier, que se passe-t-il ? Cela aura un impact sur notre évier. Il faudra soit le déplacer soit ajouter une rallonge pour que l’évier soit toujours raccordé à l’arrivée d’eau.
De la même façon, si la classe qui est utilisée change alors cela pourrait avoir un impact sur la classe qui l’utilise. Et cet impact pourrait signifier un travail conséquent.
C’est-à-dire ?
Dans le cadre de notre exemple, après avoir déplacé l’arrivée d’eau on va devoir s’assurer que cette dernière est toujours opérationnelle (donc que de l’eau arrive).
Mais nous allons également modifier l’évier en lui ajoutant une rallonge et nous allons devoir vérifier que l’évier fonctionne toujours (donc que de l’eau sort du robinet).
De la même façon, modifier une classe (A) qui est utilisée par une autre classe (B) implique :
Si nécessaire, adapter la classe (B) qui utilise la classe (A).
Vérifier que la modification sur la première classe (A) a fonctionné.
Vérifier que la deuxième classe (B), celle qui a été impactée, fonctionne toujours.
L’idée est donc de réduire le couplage car moins il y a de dépendances entre nos objets, mieux l’on se porte. Et s’il est évidemment impossible de supprimer toutes dépendances, on peut diminuer leur impact.
Durant la démonstration suivante nous allons reprendre le contexte du projet Epicrafter’s Journey. L’idée est d’avoir une classe Rempart. Un rempart est évidemment composé de blocs. Pour simplifier on considère qu’un rempart a juste besoin d’un bloc.
Quels sont les points à retenir ?
L’injection de dépendances s’appuie sur l’utilisation des interfaces où :
La classe Bloc va implémenter une interface IBloc.
La classe Rempart va typer sa dépendance avec l’interface IBloc et non la classe Bloc.
La classe Bloc ne sera pas instanciée au sein de la classe Rempart mais sera fournie à cette dernière via le constructeur (ou via un mutateur).
Le résultat de l’injection de dépendances est que :
L’interface IBloc est le contrat qui lie Rempart et Bloc.
La classe Rempart n’a plus aucune référence vers la classe Bloc.
Rempart.java
package ej;
public class Rempart {
private IBloc mur;
public Rempart(final IBloc bloc) {
this.mur = bloc;
}
}
Main.java
package ej;
public class Main {
public static void main(String[] args) {
IBloc mur = new Mur(100,100,100,true); // un mur costaud!
Rempart rempartNord = new Rempart(mur); // injection de l’instance mur dans l’instance pour le rempart au nord
}
}
On dit que la dépendance est inversée car au lieu que Rempart ait une dépendance sur Bloc, c’est Bloc qui a une dépendance vers IBloc. Lorsque nous lisons le code de la classe Rempart, il n'y a aucune référence à la classe Bloc donc aucune dépendance !
Je vais vous montrer d’autres classes particulières.
Découvrez les Records
Lorsqu’on programme il est très fréquent de vous créer des objets qui contiennent uniquement des informations (donc des attributs). Ces classes servent à modéliser ou représenter la donnée. Par exemple, pour Epicrafter’s Journey nous avons une classe Planete qui représente comme son nom l’indique une planète :
Planete.java
package ej;
public class Planete {
private String nom;
private double perimetre;
private int superficie;
Planete() { }
Planete(final String nom, final double perimetre, final int superficie) {
this.nom = nom;
this.perimetre = perimetre;
this.superficie = superficie;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public double getPerimetre() {
return perimetre;
}
public void setPerimetre(double perimetre) {
this.perimetre = perimetre;
}
public int getSuperficie() {
return superficie;
}
public void setSuperficie(int superficie) {
this.superficie = superficie;
}
}
Coder ce type de classe est assez simple mais il faut reconnaître que c’est assez verbeux, cela signifie qu’il y a beaucoup de mots à écrire.
Java offre une solution peu verbeuse depuis sa version 16 et l’ajout de classe particulière nommé record.
Reprenons notre classe Planete mais transformons là en record :
Planete.java
public record Planete(String nom, double perimetre, int superficie) { }
Et voilà, en 1 ligne !
Son utilisation désormais :
Main.java
package ej;
public class Main.java {
public static void main(String[] args) {
var terre = new Planete(“Terre”, 40075.017, 510067420);
System.out.println(terre.getNom()); // affiche Terre
}
}
Pouvoir coder un tel objet en juste une ligne est très attirant, mais un record a des caractéristiques spécifiques vis-à-vis d’une classe. Les voici :
c'est une classe finale : un record ne peut être hérité.
chaque élément fourni entre parenthèses sera de visibilité privée et final
un accesseur est automatiquement ajouté pour chaque élément
un constructeur public permettant d'initialiser les éléments est automatiquement ajouté
Un record peut néanmoins définir des méthodes et implémenter des interfaces, comme le montre cet exemple :
IPlanete.java
package ej;
public interface IPlanete {
public void afficher();
}
Planete.java
package ej;
public record Planete(String nom, double perimetre, int superficie) implements IPlanete {
@Overidde
public void afficher() {
System.out.println(“Je suis la planète “ + this. nom);
}
}
Beaucoup de développeurs n’ont pas encore pris l’habitude d’utiliser les records en Java, c’est dommage et je vous encourage à en tirer pleinement profit !
Passons maintenant à une dernière classe particulière pour répondre à un autre besoin du projet…
Découvrez les Enums
Arrêtons nous maintenant sur le fait que Jenny nous a spécifié que les blocs peuvent avoir une couleur. Or ces couleurs sont fixes, pour rappel : bleu, vert, gris, marron et noir.
Comment écrire cette liste dans le code ?
Une solution serait de créer une classe avec des attributs qui sont statiques, finaux et initialisés directement :
public classe Couleur {
public static final String blue = “BLEU”;
public static final String vert = “VERT”;
public static final String gris = “GRIS”;
public static final String marron = “MARRON”;
public static final String noir = “NOIR”;
}
C”est bien mais encore une fois ce code est assez lourd à l’écriture et nous pourrions faire mieux grâce aux énumérations ! Les énumérations sont un autre type de classe particulière en Java très utile.
Notez ce code :
public enum Couleur {
BLEU,
VERT,
GRIS,
MARRON,
NOIR
}
De nouveau nous avons un code moins long et qui sera également plus pratique. Notons comment nous pourrions l’utiliser :
Main.java
package ej;
public class Main {
public static void main(String[] args) {
Couleur choixCouleur = Couleur.VERT;
switch(choixCouleur) {
BLEU -> System.out.println(“Pour des blocs représentant l’eau”);
VERT -> System.out.println(“Pour des blocs représentant l’herbe, les feuilles, etc.”);
GRIS -> System.out.println(“Pour des blocs représentant le sol ou un mur”);
MARRON -> System.out.println(“Pour des blocs représentant la terre ou un toit”);
NOIR -> System.out.println(“Pour des blocs représentant une roche”);
}
}
}
On note que :
Une énumération est utilisable comme type pour une variable.
Pour accéder à un élément de l’énumération la syntaxe est [nom de l’énumération].[nom de l’élément]
À vous de jouer
Contexte
Vous avez maintenant l’habitude, une nouvelle tâche vous est assignée :
ID de la tâche : 6
Nom de la tâche : Ajouter une couleur aux blocs
Description de la tâche :
Chaque bloc doit pouvoir avoir une couleur. La liste des couleurs est prédéfinie : vert, bleu, gris, marron, noir
Consignes
Créer une énumération nommée Couleur et qui contient les 5 couleurs choisies.
Modifier la classe Bloc pour lui ajouter un attribut couleur qui se servira de l’énumération comme type.
Adapter le code des classes Bloc, Mur et Porte pour tenir compte de ce nouvel attribut. Par défaut, un mur est gris et une porte est bleue. Un setter est défini pour pouvoir modifier la couleur.
En résumé
Une interface peut définir des attributs qui seront automatiquement public, static et final ainsi que des signatures de méthodes.
L’injection de dépendances permet de mettre en œuvre l’inversion de dépendance et ainsi réduire le couplage entre les classes.
Les records sont des classes particulières peu verbeuses utiles pour manipuler des données immuables.
Les énumérations permettent de définir une liste fixe d’éléments.
Dans le prochain chapitre, découvrons comment manipuler des données à l’aide des collections !