Extensibilité #
Pour comprendre comment concevoir un code extensible, nous allons concevoir la première étape de développement d’une application permettant de gérer des réservations. Celles-ci comprendront des ressources. Nous souhaitons détecter si des réservations sont en conflit ; elles le sont si elles ont au moins une ressource en commun et que les réservations se chevauchent.
Nous aurons plusieurs types de ressources. Dans un premier temps, nous souhaitons gérer les réservations d’une université avec comme seules ressources des professeurs et des salles. Nous souhaitons également afficher le détail des réservations dans un terminal.
L’objectif est de manipuler des ressources, sans se préoccuper de savoir de quelle ressource il s’agit.
Version procédurale en C #
Supposons un instant que nous avons un code procédural tel que le C
pour réaliser l’application. Ceci nous permettra par la suite de mieux comprendre les avantages du polymorphisme et d’un code extensible.
Pour représenter une fonctionnalité métier, nous allons réaliser une fonction print
qui affiche dans un terminal des informations sur la ressource. Nous aimerions passer n’importe quel type de ressource à cette fonction sous cette forme :
|
|
Pour y arriver, il est nécessaire d’avoir une structure pour les professeurs, une structure pour les salles de cours et un type union
permettant d’être soit un professeur, soit une salle.
|
|
Notre type Resource
est une union. L’attribut kind
est un discriminant permettant de savoir s’il s’agit d’un professeur ou d’une salle.
Nous pouvons maintenant réaliser notre fonction d’affichage acceptant tout type de ressources.
|
|
Pour tester notre fonction, nous pouvons créer un tableau statique de ressources.
int main() {
Resource resources[3] = {
{.kind = PROF, .type = {.prof.fullname="Paul",.prof.numberPhone="62433"}},
{.kind = PROF, .type = {.prof.fullname="Oreste",.prof.numberPhone="62422"}},
{.kind = ROOM, .type = {.room.id="A406", .room.capacity=50}}
};
for(int i = 0; i < 3; i++){
print(&resources[i]);
}
return EXIT_SUCCESS;
}
L’exécution affiche le résultat ci-dessous. Le code complet se trouve ici :
professeur fullname: Paul, numberphone: 62433
professeur fullname: Oreste, numberphone: 62422
room id: A406 with capacity: 50
Que se passe-t-il si nous souhaitons ajouter une extension ? Ajoutons une nouvelle ressource permettant de représenter un étudiant et observons toutes les modifications qui doivent être effectuées.
Le type Resource
doit avoir connaissance de notre nouveau type Student
:
|
|
Ce qui est un moindre mal. Le problème réside dans la logique-métier, représenté par notre fonction print
. Celle-ci doit malheureusement être modifiée à chaque ajout d’un nouveau type de ressource.
|
|
La caractéristique d’un code extensible est de permettre d’ajouter de nouveaux types sans modifier la logique-métier. C’est ce que nous permet la POO.
Version POO en Java #
Ce que nous avons tenté de faire en C
est de faire abstraction du type de ressource. D’avoir un comportement qui est le même pour chaque ressource sans se soucier du type. Le premier outil permettant un mécanisme d’abstraction en POO est l’interface.
Définitions
Une interface, appelée parfois dans d’autres langages protocol ou trait, est un contrat qui définit les fonctionnalités et le comportement des objets qui les respectent.
Une interface en Java peut comporter:
- des méthodes abstraites (non implémentées)
- des méthodes concrètes (implémentées)
- des méthodes statiques et des constantes
Elles ne peuvent pas contenir un état: elles ne peuvent donc pas avoir de champs, ni de constructeurs.
Réalisons notre première version de l’application en Java avec les classes Professor
et Room
à l’aide d’une interface. Celle-ci aura comme contrat une méthode String info()
qui retourne des informations de la ressource sous forme de String
.
|
|
Cette fois-ci, la logique est déplacée dans l’objet. Une classe qui respecte une interface l’indique à l’aide du mot-clé implements
. Il s’agit en fait d’un héritage. Une classe A
qui respecte une interface I
hérite de celle-ci et donc un A
est un I
tout comme un Professor
est une Resource
. Il s’agit d’un héritage basé sur le comportement plutôt qu’un héritage structurel. Ce dernier est à éviter autant que possible dans un design. Nous utilisons le terme d’héritage structurel lorsqu’une classe hérite d’une autre classe.
Voyons maintenant l’équivalent de notre fonction print
:
|
|
Nous n’avons plus besoin de branchement conditionnel. Nous pouvons passer n’importe quel objet à notre méthode print
pour autant qu’il respecte le contrat défini par l’interface Resource
; il s’agit là du polymorphisme de sous-typage : permettre de remplacer un type par un sous-type.
Voyons maintenant l’équivalent de notre programme principal main
:
|
|
Nous remarquons que notre liste accepte également n’importe quel type de ressources.
Si nous souhaitons maintenant ajouter une extension avec un étudiant, il suffit d’ajouter une nouvelle classe Student
qui respecte également l’interface Resource
.
|
|
Toutes les nouveautés ont pu être intégrées à notre nouvelle classe sans modifier le reste du code! Vous pouvez accéder au code complet ici
Conclusion #
Nous avons vu par le passé un des grands avantages de la POO et du concept d’objet: celui de respecter en tout temps les règles métiers en restant dans un état cohérent.
Nous avons opposé dans ce chapitre la programmation procédurale et la programmation orientée objet pour introduire deux autres concepts fondamentaux: l’extensibilité et l’abstraction.
Ces deux caractéristiques sont conçues à l’aide d’interface, ce que j’appelle l’héritage comportemental par opposition à l’héritage structurel. Ce n’est pas parce que Professor
et Student
partage des champs communs (le champ fullname
en l’occurrence) qu’il faille réaliser une classe Person
. L’héritage est utilisé pour adopter des comportements différents en fonction d’un type ou d’une stratégie et pour éviter la duplication de code logique. Faire le choix de l’héritage pour un attribut est une très mauvaise idée et nous verrons plus tard un cas d’école pour bien le comprendre.
Gardez en mémoire qu’un héritage, quelle qu’il soit, est un couplage fort et qu’il induit une dépendance très forte: il annonce publiquement une dépendance alors que l’encapsulation a pour objectif de le cacher. Soyez prudent !
Dès les années 60, les premiers pionniers en informatique écrivaient déjà qu’un bon design nécessite un couplage faible et une cohésion forte. Tout l’intérêt de l’encapsulation et du polymorphisme réside dans cette idée. Si le polymorphisme est obtenu grâce à l’héritage, c’est ce premier qu’il faut viser comme objectif et non l’inverse.
Nous allons présenter la syntaxe générale des interfaces dans une prochaine section .