Atelier 05
Dans ce laboratoire, vous allez réviser les génériques et mettre en pratique une implémentation du patron d’observateur.
Génériques
Supposons le modèle d’objets suivant :
Des chemins de fer traversent le pays du lait et du miel.
Les trains transportent du lait et du miel (qui sont tous deux des produits agricoles) des fermes vers les villes.
Mais les habitants du pays du lait et du miel sont aussi très branchés technologie et exécutent beaucoup de logiciels d’intelligence artificielle et de chaîne de blocs, donc ils ont également besoin de trains pour transporter des combustibles fossiles (charbon, pétrole) et du minerai d’uranium des mines vers les centres de serveurs.
Essentiellement, on peut imaginer la hiérarchie de classes suivante :
classDiagram
class cargo
class produce
class fuel
class milk
class honey
class coal
class petrol
class uranium
cargo <|-- produce
cargo <|-- fuel
produce <|-- milk
produce <|-- honey
fuel <|-- coal
fuel <|-- petrol
fuel <|-- uranium
Pour des raisons de sécurité, un train ne peut jamais transporter des wagons contenant à la fois du carburant et des produits agricoles. C’est-à-dire :
- Lorsqu’un
new Trainest créé, il doit préciser dès sa création pour quelle catégorie deCargoil est destiné, en utilisant un générique. - Chaque fois qu’un
Wagonest ajouté à unTrain, leTraindoit utiliser la sécurité de type pour s’assurer que la cargaison duWagoncorrespond à la catégorie de cargaison duTrain.
Exemples :
Pour notre implémentation de Wagon utilisant des génériques, le train doit appliquer la sécurité de type et empêcher l’ajout de Wagons d’un type incorrect :
package milkAndHoney;
// All wagons are for a specific form of Cargo.
public class Wagon<T extends Cargo> {
}
Les wagons de produits agricoles ne peuvent être ajoutés qu’à un train de produits agricoles,
et les wagons de carburant ne peuvent être ajoutés qu’à un train de carburant :
Wagon<Milk> m1 = new Wagon<>();
Wagon<Honey> h1 = new Wagon<>();
Wagon<Coal> c1 = new Wagon<>();
Wagon<Petrol> p1 = new Wagon<>();
Wagon<Uranium> u1 = new Wagon<>();
produceTrain.addWagon(m1);
produceTrain.addWagon(h1);
produceTrain.addWagon(u1); // <== Compiler must reject this.
fuelTrain.addWagon(c1);
fuelTrain.addWagon(p1);
fuelTrain.addWagon(u1);
fuelTrain.addWagon(m1); // <== Compiler must reject this.
À vous de jouer
Implémentez une classe Train qui stocke des Wagons, où chaque Wagon transporte une cargaison d’un type spécifique.
Assurez-vous que Train offre des méthodes addWagon et getWagon qui appliquent la sécurité de type au niveau du compilateur pour les paramètres d’entrée et les types de retour.
Testez votre implémentation en créant d’abord deux trains : un pour les produits agricoles et un pour le carburant :
Train produceTrain: Vérifiez que le compilateur vous permet uniquement d’ajouter desWagons contenant des sous-classes de produits agricoles.Train fuelTrain: Vérifiez que le compilateur vous permet uniquement d’ajouter desWagons contenant des sous-classes de carburant.
Ne créez pas de classes distinctes
Vous pourriez être tenté de simplement définir deux classes de train distinctes — une pour Produce et une pour Fuel — mais l’intérêt des génériques est de maintenir la sécurité de type avec une seule classe Train.
Le principe d’Hollywood
Le principe d’Hollywood se résume à : « Ne nous appelez pas, c’est nous qui vous appellerons ! »
- Ne demandez pas sans cesse si quelque chose s’est déjà produit.
- Attendez simplement d’être notifié.
Dans ce deuxième exercice, vous appliquerez le principe d’Hollywood pour observer la fission d’un isotope instable.
- L’implémentation de l’isotope est fournie ici.
- Elle est assez simple à utiliser. Par exemple, pour créer un isotope d’azote-16, on peut utiliser :
UnstableIsotope nitrogen16 = new UnstableIsotope(4);
La classe possède un indicateur interne permettant de savoir si elle s’est déjà désintégrée.
Le problème avec la fission est que nous ne savons pas exactement quand elle se produira : cela peut être presque instantané, ou prendre une minute.
Nous savons seulement que cela finira par arriver.
La « mauvaise » façon d’implémenter un détecteur de fission consiste à vérifier sans arrêt si l’atome s’est déjà désintégré :
public class DecayDetector {
public void tellWhenDecayed(UnstableIsotope isotope) {
// Here we check twice per second if the atom has already decayed. This is a polling
// implementation:
while (!isotope.isDecayed()) {
System.out.println("Atom not yet decayed.");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("Atom decayed.");
}
}
Dans une classe de lancement, on peut d’abord créer l’isotope, puis observer sa désintégration :
main() {
// First create the isotope, with half-life duration 4 seconds
UnstableIsotope nitrogen16 = new UnstableIsotope(4);
// Then observe its decay
DecayDetector detector = new DecayDetector();
detector.tellWhenDecayed(nitrogen16);
}
Cela fonctionne parfaitement, mais c’est une utilisation inefficace des ressources : * la plupart du temps, rien n’est détecté ; * une fréquence d’interrogation plus élevée donne plus de précision, mais augmente la charge du processeur.
Une approche bien meilleure consiste à utiliser le patron observateur :
Nous n’interrogeons plus du tout !
Lors de la désintégration, l’atome notifie simplement ses observateurs.
À vous de jouer
- Examinez l’interface
RadiationObserver, qui contient une seule méthodepickupRadiation(). - Faites en sorte que la classe
DecayDetectorimplémente l’interfaceRadiationObserver.- Implémentez toutes les méthodes requises.
- Supprimez la méthode
tellWhenDecayedbasée sur l’interrogation.
- Utilisez le constructeur surchargé de
UnstableIsotopepour passer leDecayDetectorcomme deuxième argument du constructeur. - Ajoutez un paramètre
RadiationObserverau constructeur deAtom. - Vérifiez que votre observateur fonctionne toujours, sans avoir recours à l’interrogation.
Pourquoi une interface ?
Pourquoi ne pas simplement faire en sorte que UnstableIsotope notifie directement le DecayDetector, sans interface intermédiaire ?
Parce qu’un atome n’a rien à voir avec un détecteur et ne devrait pas le connaître.
Un atome peut seulement émettre de la radiation, et un détecteur peut seulement la capter.