Skip to content

Lab 05

In this lab you'll be revising generics and practice an implementation of the observer pattern.

Generics

Lets assume the following object model:

Railroads run through the land of milk and honey. Trains bring milk and honey (which are both produce) from the farmlands to the cities. But the residents of the land of milk and honey are also technologically savvy and run a lot of AI and blockchain software, so they also need trains to transport fossile fuel (coal, petrol) and uranium ore from mines to the server farms.

Essentially we can imagine the following class hierarchy: 2

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

For safety reasons, a train may never transport wagons with both fuel and produce. That is:

  • Whenever a new Train is created, it must specify at creation for which Cargo category it is intended for, using a generic.
  • Whenever a Wagon is added to a Train the Train must use type safety to ensure the Wagons cargo falls into the Trains cargo category.

Examples:

For our Wagon implementation using generics, the train must apply safety and prevent adding Wagons of the wrong type:

package milkAndHoney;

// All wagons are for a specific form of Cargo.
public class Wagon<T extends Cargo> {

}

Produce wagons can only be added to a produce train, fuel wagons can only be added to a fuel train:

    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.

Your turn

Implement a class Train storing Wagons, where each Wagon has a cargo of a specific type. Make sure Train offer an addWagon and getWagon method that enforces compiler level type safety for input parameters and return types.

Test your implementation by frist creating two trains, one for produce, one for fuel:

  • Train produceTrain: Verify the compiler lets you only add Wagons with produce subclasses.
  • Train fuelTrain: Verify the compiler lets you only add Wagons with fuel subclasses.

Do not create separate classes

You might be tempted to just define two distinct train classes, one for Produceand one for Fuel - but the interest of generics is to maintain type-safety with just a single Train class.

The Hollywood principle

The hollywood principle comes down to: "Don't call us we call you!".

  • Do not repeatedly ask if something has already happened.
  • Lay back and wait for being notified.

In this second exercise you'll be applying the hollywood principle to observe fission of an unstable isotope.

  • The isotope implementation is provided here.
  • It is fairly simple to use. For example to create a nitrogen-16 isotope we can use:
    UnstableIsotope nitrogen16 = new UnstableIsotope(4);

The class has a built in flag, to tell us whether it has already decayed. The problem with fission is, we do not know when exactly it happens. Could be almost instantly, or in a minute. We only know eventually it will happen. The "bad" way of implementing a fission detector, is by repeatedly checking if the atom has already decayed:

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.");
  }
}

In a launcher class, we can first create the isotope, then observe its decay:

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);
}

This perfectly works, but it is wasteful in resources:

  • most of the time nothing is detected
  • a higher polling frequency gives more accuracy, but increases CPU load.

The far better approach is using the observer pattern: We don't poll at all! On decay, the atom notifies its observers.

Your turn

  • Inspect the RadiationObserver interface, with a single method pickupRadiation()"
  • Have the DecayDetector implement the RadiationObserver interface.
    • Implement all required methods.
    • Remove the poll-based tellWhenDecayed method.
  • Use the UnstableIsotopes overloaded constructor to pass the DecayDetector as second constructor argument.
  • Add RadationObserver parameter to the Atom's constructor.
  • Verify your observer still works, without polling.

Why the interface

Why not just have the UnstableIsotope notify the DecayDetector, without intermediate interface ? Because an atom has nothing to do with detectors, and should not know about them. An atom can only radiate and a detector can only pickup radiation.