L’organisation en classes et en packages permet de bien organiser les "cercles de responsabilité" d’un code (qui est responsable de quoi) mais ne suffit pas pour mettre en place une modularité sophistiquée.
A l’intérieur d’un projet on peut répartir le travail et les responsabilités
de chaque équipe mais lorsque l’ensemble de packages est mis à la disposition
d’une organisation "extérieure" tous les aspects public
de ces packages se trouvent exposés.
On devrait pouvoir limiter les points d’entrée réservés à ces codes extérieurs. Par exemple: avoir des accès "public" réservés uniquement à d’autres packages de la même organisation (sans être "publics" pour tout le monde!)
Il peut y avoir plusieurs raisons de faire ça: la plus immédiate est de préserver
les possibilités d'évolution des codes vis-à-vis de codes "extérieurs".
Normalement un code public
ne devrait pas pouvoir changer sa signature
une fois qu’il est mis à la disposition de code "clients "extérieurs;
toute modification
impacterait ces codes … mais si ces codes "clients" n’ont justement pas accès
à des services qui sont considérés comme "interne" à la logithèque
alors on a complété un niveau supplémentaire de modularité (et on s’est réservé
le droit de faire évoluer les A.P.I.s internes au projet).
Souvent (mais pas nécessairement) la mise en place de blocs de logithèques va correspondre
à une frontière entre organisations. Un fournisseur de codes va offrir
différents modules
et une autre organisation va utiliser certains de ces super-composants
en se limitant à quelques points d’entrée. (Il y a en fait d’autres cas de figure - comme par exemple
le fait que les logithèques java ont été dispersées en différents modules
pour éviter d’avoir à charger à l’exécution l’ensemble de ces codes -.
Ici, nous essayerons d’introduire le plus simplement possible cette notion de module
au sens java).
![]() | |
L’organisation modulaire apporte certes des avantages mais aussi des inconvénients en terme de complexité! Donc son adoption (du moins au niveau de java 9) devrait être soigneusement planifiée: il faut connaître les aspects de la technologie mais ne l’adopter que quand le projet est mûr pour en tirer des avantages (et peut-être attendre des versions de java ultérieures avec des outils qui simplifieraient les opérations de création d’applications autonomes). |
Supposons que nous écrivions un code de nom canonique com.maboite.monapp.ihm.Fenetre
Nous allons créer un module
de nom com.maboite.monapp
(ce n’est pas illogique: dans le cadre
de l’organisation com.maboite
on aura un ensemble de codes regroupés dans le cadre de monapp
.
Maintenant ce n’est pas non plus strictement obligatoire si ce module ne constitue pas un point d’entrée
pour d’autres codes - si ce module reste "interne" à l’organisation - ).
Les répertoires sources seront alors organisés de la manière suivante à partir
du répertoire de référence des sources (que nous appellerons projet
).
projet |_ com.maboite.monapp -> module-info.java |_ com |_ maboite |_ monapp |_ ihm -> Fenetre.java
Fenetre.java
sera un code graphique (ihm
= Interactions Homme/machine!). Ce code va faire appel
à des classes du package standard java.awt
.
Ce package ne se trouve pas dans le module standard java.base
qui est automatiquement accessible
mais dans le module standard java.desktop
Le répertoire com.maboite.monapp
on va trouver un fichier source spécial de nom module-info.java
et qui contient
la description du module.
module com.maboite.monapp { requires java.desktop; }
Ici:
java.desktop
En fait la directive requires
implique que :
ClassLoader
du module demandeur pourra "lire" les codes exportés par ce module (readability).
(mais il peut aussi lire des codes qui ne sont pas explicitement importables)
ClassLoader
permette d’accéder à son code.
(point que nous allons détailler un peu plus loin).
Le module-info
de java.desktop
est assez compliqué mais en voici un extrait:
module java.desktop { // il a lui même besoin d'un autre module requires java.prefs; // donne accès au module datatransfer qu'il utilise requires transitive java.datatransfer; // ... autres lignes // ici les déclaration d'exportation // ces packages deviennent visibles de l'extérieur exports java.applet; exports java.awt; // .... etc. etc...
Le code de Fenetre.java
au sein de notre module pourra alors avoir accès aux A.P.I
des packages visibles dans le module desktop
(plus, éventuellement, à des classes venant
de datatransfer
qui lui auront été passés par des codes de desktop
!)
![]() | |
L’utilisation de
|
Maintenant les compilations!
Il est de bonne guerre de ne pas mélanger les fichiers source et les
fichiers binaires. On peut , par exemple, créer un répertoire quelque part
pour abriter les binaires (bien noter que dans les cas des IDE ce répertoire est créé pour vous avec un nom propre
à l’outil - par ex. out
- ).
On va donc créer un répertoire modbin
et un sous-répertoire pour le module:
modbin |_ com.maboite.monapp
Maintenant une version (simplifiée) de la commande de compilation:
javac -d
répertoire_modbin_com.maboite.monapp module-info.java
javac -d
répertoire_modbin_com.maboite.monapp Fenetre.java
Si tout se passe bien cela doit générer un arborescence de fichiers comme celle-ci:
modbin |_ com.maboite.monapp -> module-info.class |_ com |_ maboite |_ monapp |_ ihm -> Fenetre.class
Faisons un premier essai d’utilisation du code de Fenetre
depuis JShell:
En se positionnant dans le répertoire modbin
(pour simplifier l’exposé):
jshell --module-path .
Exécution:
jshell> new com.maboite.monapp.ihm.Fenetre("Hello") ; | Error: | package com.maboite.monapp.ihm is not visible | (package com.maboite.monapp.ihm is declared in module com.maboite.monapp, which is not in the module graph) | new com.maboite.monapp.ihm.Fenetre("Hello") ; | ^--------------------^
Ici rien que de très normal du point de vue des principes des modules
java:
nous sommes "à l’extérieur" du module com.maboite.monapp
et le package
ihm
n’est pas "exporté".
Essayons d’une autre manière de façon à permettre à Jshell de tester notre code (toujours dans le répertoire bin
):
jshell --class-path com.maboite.monapp
new com.maboite.monapp.ihm.Fenetre("Hello") ; $1 ==> com.maboite.monapp.ihm.Fenetre@23fe1d71
Ici l’appel a fonctionné!
Pour lancer java
nous avons besoin d’une classe contenant un main
.
Si nous créons une classe Main
(avec la méthode public static void main(string[] args)…
)
dans un package com.maboite.monapp.mains
alors nous pourrons invoquer java (ici depuis le répertoire bin
)
java -p . -m com.maboite.monapp/com.maboite.monapp.mains.Main
Ici:
-p
(ou --module-path
) indique l’emplacement des modules
-m
permet d’indiquer le module hôte de la classe principale.
Ici nous avons spécifié explicitement quelle était cette classe principale mais nous verrons ultérieurement
qu’il y a un autre moyen de faire connaître cette information à l’exécution.
Comme pour jshell on aurait pu simplifier une invocation de test en se rendant
dans le répertoire binaire com.maboite.monapp
et en lançant:
java com.maboite.monapp.mains.Main
Si tout ceci vous semble un peu compliqué retenez les points suivants:
jar
que nous étudierons plus loin.
Chaque archive contient un module: à peu de choses près tout ce qui se trouve
dans le répertoire des binaires auquel on a donné le nom d’un module.
Il contiendra donc module-info.class
, les arborescences binaires de packages
et d’autre éléments complémentaires.
class-path
).
En fait le terme "sécurité" doit être replacé dans un contexte analogue à que ce que l’on fait en marquant
des membres private
: ce que l’on gère c’est des cercles de responsabilité vis à vis des codes.
On définit des frontières qu’il n’est pas recommandé de franchir! … Ce ne sont pas des barrages
avec des droits d’accès, ce sont des points qui marquent des limites de responsabilité: celui qui viole
ces limites le fait en pleine connaissance et à ses risques et périls (mais par exemple un test par le développeur lui même
ne devrait pas prêter à conséquence).
Pour bien comprendre cette notion de responsabilité associée aux modules voir les exercices sur les modules
qui accompagnent la leçon suivante.
![]() | Attention |
---|---|
En général les outils interactifs d’aide à la programmation utilisent une structure analogue à celle décrite ici. Prendre toutefois garde au fait que les binaires que vous voulez livrer ne sont pas nécessairement ceux que vous utilisez pour votre développement au jour le jour (et qui sont compilés avec toutes les options de debug). Il peut s’avérer nécessaire de différencier la génération des codes en cours de développement
de la génération des codes à livrer. (voir à ce propos les options de |
Le compilateur java ne fait pas que vérifier si la syntaxe du code est correcte. Il vérifie aussi si l’utilisation des autres codes est conforme aux A.P.Is.
Pour cela le compilateur doit savoir trouver les binaires correspondants (on peut également compiler plusieurs sources interdépendantes en même temps).
Il faut donc indiquer au compilateur où trouver les binaires.
Ceci se fait par l’option --module-path
suivi d’une liste de répertoires
dans lesquels se trouvent les codes de modules recherchés (donc le répertoire modbin
dans notre exemple.
S’il y avait plusieurs répertoires de ce genre on les met dans un liste de noms selon la syntaxe de path
propre à votre plateforme:
rep1:rep2:rep3
sur les systèmes de type UNIX; rep1;rep2;rep3
sur les sytèmes de type WIN.
ATTENTION: pour un code dans un module qui fait appel à des codes d’autres modules
il faut ben s’assurer que ces autres modules sont bien présents dans la clause requires
du
fichier module-info
local!
Par exemple:
javac --module-path
repertoire_modbin -d
repertoire_modbin/com.maboite.uneautreapp module-info.java
(ici si un requires
de com.maboite.uneautreapp
spécifie un module on doit le trouver dans bin
)
javac --module-path
repertoire_modbin -d
repertoire_modbin/com.maboite.uneautreapp AutreClasse.java
Il existe de nombreuses autres façons d’utiliser les options de compilation mais nous n’allons pas les détailler dans ce document.
Il est possible de ne pas utiliser de modules et donc il est prévu de gérer ce type de code. En fait c’est surtout fait pour des codes antérieurs à la version 9.
De tels codes sont censés appartenir à un module sans nom (unnamed module
).
Mais il existe aussi des codes non-modulaires qui seront reconnus comme appartenant à un "module automatique"
(sur lesquels on peut faire explicitement des requires
par ex.) nous verrons cet aspect avec la leçon sur les jars.
Ces codes correspondent à une organisation des sources comme:
projet |_ com |_ acme |_ ventes |_ ShowBiz.java
et pour les binaires déployés dans le système de fichier:
bin |_ com |_ acme |_ ventes |_ ShowBiz.class
Pour la J.V.M. chargée d’exécuter ce code le CLASSPATH
devra comprendre
ce répertoire bin
. Le ClassLoader
qui va rechercher la classe com.acme.ventes.ShowBiz
va explorer de manière appropriée la hiérarchie des répertoires et trouver le fichier binaire
ShowBiz.class
.
Note importante: l’appel à javac
devra aussi avoir le répertoire classes
dans son CLASSPATH
(cela permettra de résoudre les références à d’autres codes
comme com.acme.produits.trucs.CycloRameur
).
javac --class-path
repertoire_bin -d
repertoire_bin ShowBiz.java
![]() | |
Point de terminologie: on dit que le répertoire |
![]() | |
L’option --class-path peut référencer plusieurs répertoires : -cp unixdir1:unixdir2:unixdir3 -cp windir1;windir2;windir3 (En fait la liste peut aussi contenir des archives java -archives jar-) |
Que faire si on mélange des codes avec modules et des codes sans modules?:
modbin
pour les binaires modulaires et bin
pour les codes non modulaires
nous verrons que par contre cette séparation des répertoires sera utile quand on déploiera des archives jar).
Depuis un code de modules on peut utiliser l’option --class-path
pour trouver les codes sans modules.
De plus les packages des codes du module sans nom sont automatiquement acceptables (pas de requires
nécessaire).
javac --module-path
repertoire_modbin --class-path
repertoire_bin -d
repertoire_modbin_module MaClasse.java
Le module-path
est pour les modules; le class-path
est pour les packages dans le module par défaut.
Utiliser des codes de module depuis un module sans nom (par ex. pour des tests) est plus problématique.
javac --module-path
repertoire_modbin --add-modules ALL-MODULE-PATH -d
repertoire_bintest ClassTest.java
Rappel: les déploiements des librairies de modules et de packages se font en utilisant des archives JAR que nous verrons ultérieurement.
--Exercices sur les modules--
Pour des raisons techniques les exercices sur les modules seront reportés à la leçon suivante sur "composition, association, héritage". Les points abordés dans cette leçon permettront de mieux comprendre le fonctionnement d’ensemble des relations entre codes.