The idea is to have the context object implement a different interface for each level it's used at, exposing only the getters for meaningful values at that level, and methods for turning the context into a more specialized one. These methods set some variables and then return the same object under a different interface. It's a bit of work, but it can all go into one well-structured file, and it keeps the code and semantics nice and clean.
As an example, let's say we're rendering a World containing Ships containing Crew. At the world level, we just need to know the global illumination value. At the ship level, we also want the computed location of the ship on screen. Finally, at the crew level, we additionally want to know if we should show detailed crew stats.
Resulting in the following code:
public final class RenderCtx {
public interface Crew {
public double lightIntensity();
public double shipX();
public double shipY();
public boolean crewStatsVisible();
}
public interface Ship {
public double lightIntensity();
public double shipX();
public double shipY();
public Crew crewCtx(boolean crewStatsVisible);
}
public interface World {
public double lightIntensity();
public Ship shipCtx(double shipX, double shipY);
}
public World get(double lightIntensity) {
return new Impl(lightIntensity);
}
private RenderCtx() {} // Can't create directly.
private static final class Impl implements Crew, Ship, World {
double lightIntensity;
double shipX, shipY;
boolean crewStatsVisible;
Impl(double lightIntensity) { this.lightIntensity = lightIntensity; }
@Override public double lightIntensity() { return lightIntensity; }
@Override public double shipX() { return shipX; }
@Override public double shipY() { return shipY; }
@Override public boolean crewStatsVisible() { return crewStatsVisible; }
@Override
public Crew crewCtx(boolean crewStatsVisible) {
this.crewStatsVisible = crewStatsVisible;
return this;
}
@Override
public Ship shipCtx(double shipX, double shipY) {
this.shipX = shipX;
this.shipY = shipY;
return this;
}
}
}
// Usage example.
RenderCtx.World wc = get(0.8); // Get top-level context.
wc.lightIntensity(); // Can only access light intensity.
RenderCtx.Ship sc = wc.shipCtx(3.8, -103.1); // Specialize into ship context.
sc.shipX(); // Can now also see ship screen coordinates.
sc.lightIntensity(); // And stuff from the higher context.
RenderCtx.Crew cc = sc.crewCtx(true); // Specialize again. Remains same object.
cc.crewStatsVisible(); // Context is mutable but behaves as if it weren't.
Now I haven't yet implemented this in Airships, though I likely will, at which point I'll report back. Meanwhile - comments? Is this a sensible way of doing things? Over-engineered? Am I missing some trick?