Aller au contenu

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 Train est créé, il doit préciser dès sa création pour quelle catégorie de Cargo il est destiné, en utilisant un générique.
  • Chaque fois qu’un Wagon est ajouté à un Train, le Train doit utiliser la sécurité de type pour s’assurer que la cargaison du Wagon correspond à la catégorie de cargaison du Train.

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 des Wagons contenant des sous-classes de produits agricoles.
  • Train fuelTrain : Vérifiez que le compilateur vous permet uniquement d’ajouter des Wagons 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éthode pickupRadiation().
  • Faites en sorte que la classe DecayDetector implémente l’interface RadiationObserver.
    • Implémentez toutes les méthodes requises.
    • Supprimez la méthode tellWhenDecayed basée sur l’interrogation.
  • Utilisez le constructeur surchargé de UnstableIsotope pour passer le DecayDetector comme deuxième argument du constructeur.
  • Ajoutez un paramètre RadiationObserver au constructeur de Atom.
  • 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.