Goals:

When the number of strategy changes, we want to minimize change. When the implementation of a strategy changes, we want to minimize change.

Case study:

  • Mode of transportation
  • Representation of a data
  • Behaviour of an actor
  • Trading algorithm

Entities/Objects involved

  • There is a strategy interface encapsulating the behaviour
  • There is a context which wraps the strategy and is responsible for getting the data to apply the strategy on.
  • There are implementation of the strategy interface.

Motivating problem

Suppose you have a set of datapoints, and you want to visualize them. You can either visualze them as a frequency graph, or as a time chart.

interface VisualizeStrategy {
    void visualize(List<DataPoint> dps)
}
class FrequencyGraph() extends visualizeStrategy { ... }
class TimeChart() extends visualizeStrategy { ... }
class ReportContext(VisualizeStrategy visualizeStrategy) {
    public void generateReport(List<DataPoint> dps) {
        visualizeStrategy.visualize(dps);
    }
}

Complications

Here we noticed that we assume all DataPoint can be visualized via frequency graph and time chart. What if DataPoint is an abstract class? And there are actually two possible types of data: one with time series and one without?

abstract class DataPoint {}
class TimeSeriesData extends DataPoint {}
class OneDimensionDataPoint extends DataPoint {}

Obviously in the TimeChart implementation we somehow have to ensure that we only get data that we can handle. For example, this could be done by doing a downcast

public void Visualize(List<DataPoint> dps) {
    Stream<TimeSeriesData> supportedData = dps.stream().map(x -> {
        if (!(x instanceof TimeSeriesData)) {
            throw new IllegalArgumentException("Unsupported data"); // Awkward
        } else {
            return (TimeSeriesData) x;
        }
    });
    // do things
}

How do we get the compiler to help us prevent the following case?

public void BadReportMain() {
    ReportContext r = new ReportContext(new TimeChart);
    r.generateReport(new OneDimensionDataPoint()); // This causes an exception 
}

This is were self referential generics can help.

Self referential generics

Let’s look at some real life examples of this

public abstract class Enum<E extends Enum<E>> {...}

But unfortunately enum is some special snowflake introduced in Java 1.5 in 2004 and if you try to create a class that extends it you’ll get an error saying Class cannot directly extends Enum Class

Anyway, the benefit of defining these self referential generics is the ability to refer to the subtype in the parent type. Think of it as attaching an extra piece of information.

abstract class DataPoint<D extends DataPoint<D>> {}
class TimeSeriesData extends DataPoint<TimeSeriesData> {}
class OneDimensionData extends DataPoint<OneDimensionData> {}

interface Visualizer<SupportedData extends DataPoint> {
    void visualize(List<SupportedData> dps);
}

class TimeChart implements Visualizer<TimeSeriesData> {
    public void visualize(List<TimeSeriesData> supportedData) {
        // Note that there is no need to do any casting
    }
}

Conclusion

Self referential generics is very confusing.