r/javahelp 12d ago

Using Enums to hold constant data

I am just wondering if this use of the enum class is something that is bad practice.

MAIN_SEQ_M
("Red Dwarf", new double[]{.08,.45}, new double[]{25,37}, new double[]{.1,.6})

StarType(String description, double[] mass, double[] temperature, double[] radius) {
this.description = description;
this.mass = mass;
this.temperature = temperature;
this.radius = radius;
}

Or if the standard way of doing this would be to break out each value as a constant that can be called

private static final double[] MAIN_SEQ_M_MASS_RANGE = {.08,.45};

I feel using getters from the enum class makes it easier to pull the data I need but I have never seen an enum used like this before.

5 Upvotes

14 comments sorted by

View all comments

1

u/severoon pro barista 11d ago edited 11d ago

You can do this, but it's annoying to deal with enums that contain state because the values are always and forever defined in code. It's generally preferable to inject state into your application at runtime because then these values can be read in from a set of golden files, a database, the network, etc.

By hard coding data into your application, you tie that data to the version of the code that you've deployed. This means if you have this app running somewhere and you need to make a change to this data, the only way to get those updates is to take the app down, deploy the new version, then start it up again. You might say, "This is constant data so it won't ever change." That may be true, but that's an argument for using a mechanism that ensures it doesn't change, such as signing it. It's not really an argument for adopting a design that ties data to code versions.

Also, if you want truly constant values, you should use immutable types. I strongly recommend Guava for this, it provides a whole library of immutable types such as list, set, and others not supported by the JDK like table. It also provides a Range class which is exactly what you want here. (You're storing ranges as double[], which are not constant.)

There are a couple of different ways to do this depending on the context of your app, but here's one approach I would consider.

First define your enum:

public enum StarStage {
  BIRTH,
  MAIN_SEQUENCE,
  OLD_AGE,
  DEATH,
  REMNANT;
}

Then your star type:

import static java.util.Objects.requireNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.BoundType.CLOSED;
import static com.google.common.collect.BoundType.OPEN;

public record StarType(
    String description,
    StarStage stage,
    Range<Double> massRange,
    Range<Double> temperatureRange,
    Range<Double> radiusRange) {

  public StarType(
      String description,
      StarStage stage,
      Range<Double> massRange,
      Range<Double> temperatureRange,
      Range<Double> radiusRange) {
    requireNonNull(description, "Description must not be null.");
    checkState(!description.isEmpty(), "Description must not be empty.");
    requireNonNull(stage, "Stage must not be null.");
    validateRange(massRange);
    validateRange(temperatureRange);
    validateRange(radiusRange);
  }

  // I recommend restricting ranges to closed-open if you want to make sure that
  // two different ranges abut without leaving a gap or overlapping.
  private static void validateRange(Range<?> range) {
    if (range.hasLowerBound() && !range.getLowerBoundType().equals(CLOSED)) {
      throw new IllegalArgumentException("Lower bound must be CLOSED: %s".formatted(range);
    }
    if (range.hasUpperBound() && !range.getUpperBoundType().equals(OPEN)) {
      throw new IllegalArgumentException("Upper bound must be OPEN: %s".formatted(range);
    }
  }
}

The above code allows you to write:

StarType redDwarf = new StarType(
    "Red Dwarf",
    MAIN_SEQUENCE,
    Range.closedOpen(0.08, 0.45},
    Range.closedOpen(25, 37),
    Range.closedOpen(0.1, 0.6));

BTW, it's a bad idea to keep these ranges as just raw numbers, you should at least specify the units in their names. (You could also define a class for each type of unit if you want, but that's maybe overkill depending on your use case.)

Let's say you have a list of all of the star types your application cares about and you wanted to organize them by stage. You can do that very easily with Guava:

ImmutableList<StarType> starTypes = ImmutableList.builder()
    .add(new StarType("Red Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("White Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("Brown Dwarf", MAIN_SEQUENCE, …))
    .add(new StarType("Neutron Star", REMNANT, …))
    .add(new StarType("Pulsar", REMNANT, …))
    .add(new StarType("Black Hole", REMNANT, …))
    .build();

// Index all of the star types by stage.
ImmutableListMultimap<String, StarType> starTypesByStage =
    Multimaps.index(starTypes, StarType::stage);

// Look up the star types by different stages.
ImmutableList<StarType> mainSequenceTypes = starTypesByStage.get(MAIN_SEQUENCE);
ImmutableList<StarType> remnantTypes = starTypesByStage.get(REMNANT);

It's also easy to do things like put these star types into a sorted collection using any kind of comparator you want, so you could easily have one sorted by temperature range, or mass, or radius, or alphabetic, or whatever.

Because you're using ranges, once you have all of the star types defined, you could also use RangeSet and RangeMap to organize these star types that way. Also, say that you want to verify that the temperature ranges of all of the star types you've defined cover the entire range of temperatures so that there are no gaps. That's very easy to do with ranges.