user1884155
user1884155

Reputation: 3736

Adding subcategories to a java Enum

Suppose I have a simple Java Enum:

public Enum itemType
{
    FRUITS("fru"),
    VEGETABLES("veg"),
    LIQUOURS("liq"),
    SODAS("sod");

    private String dbCode;

    public ItemType(String dbCode){
        this.dbCode = dbCode;
    }

    public String getDbCode(){
        return this.dbCode;
    }
}

I would now like to introduce a "category" to this enum, for example to make the distinction between liquid items and solid items. I found two ways of doing this within the enum class, see below. However, both suffer from the same anti-pattern: if the amount of categories or amount of items ever increases/decreases (imagine 100 item types with 10 categories!), I've got a lot of updating to do. What patterns can I use to design this enum as cleanly and re-usable as possible?

First approach: Add additional properties to the enum

public Enum itemType
{
    FRUITS("fru",false),
    VEGETABLES("veg",false),
    LIQUOURS("liq",true),
    SODAS("sod",true);

    private String dbCode;
    private boolean liquid;

    public ItemType(String dbCode, boolean liquid){
        this.dbCode = dbCode;
        this.liquid = liquid;
    }

    public String getDbCode(){
        return this.dbCode;
    }
    public boolean isLiquid(){
        return this.liquid;
    }
}

Second approach: Use static methods to ask about subcategories

public Enum itemType
{
    FRUITS("fru"),
    VEGETABLES("veg"),
    LIQUOURS("liq"),
    SODAS("sod");

    private String dbCode;

    public ItemType(String dbCode){
        this.dbCode = dbCode;
    }

    public String getDbCode(){
        return this.dbCode;
    }

    public static boolean isLiquid(ItemType type){
        switch(t){
            case SODA:
            case LIQOURS: return true;
            default: return false;
        }
}

Upvotes: 1

Views: 1869

Answers (4)

YoYo
YoYo

Reputation: 9405

These were three excellent answers, but I think I can combine all three in one nice package:

public enum ItemType {
    FRUITS("fru",PERISHABLE),
    VEGETABLES("veg",PERISHABLE),
    LIQUOURS("liq",LIQUIDS),
    SODAS("sod",LIQUIDS),
    FRESH_SQUEEZED_ORANGE_JUICE("orgj",LIQUIDS,PERISHABLE);

    private final String dbCode;
    private final EnumSet<ItemCategory> categories;
    private static final Map<ItemCategory,Set<ItemType>> INDEX_BY_CATEGORY = new EnumMap<>(ItemCategory.class);

    ItemType(String dbcode,ItemCategory... categories) {
      this.dbCode = dbcode;
      this.categories = EnumSet.copyOf(Arrays.asList(categories));
      //for (ItemCategory c:categories) {
      //  // Illegal Reference to Static Field!
      //  INDEX_BY_CATEGORY.put(c, this);
      //}
    }

    static {
      for (ItemCategory c:ItemCategory.values()) {
        INDEX_BY_CATEGORY.put(c, EnumSet.noneOf(ItemType.class));
      }
      for (ItemType t:values()) {
        for (ItemCategory c:t.categories) {
          INDEX_BY_CATEGORY.get(c).add(t);
        }
      }
    }

    public boolean is(ItemCategory c) {
      return INDEX_BY_CATEGORY.get(c).contains(this);
    }

    public Set<ItemType> getAll(ItemCategory c) {
      return EnumSet.copyOf(INDEX_BY_CATEGORY.get(c));
    }

    public String getDbCode() {
      return dbCode;
    }
}

Now,

  • we can easily ask about additional subcategories without writing the code for it: boolean isVegetableLiquid = VEGETABLES.is(LIQUIDS);
  • we can easily assign not only one, but multiple categories to an item as you can see for FRESH_SQUEEZED_ORANGE_JUICE.
  • we are using EnumSet and EnumMap for performance, including their methods like contains.
  • we absolutely are minimizing the amount of code required to add an additional item. This could be further minimized by setting this up by database or configuration. However, in that case we would have to avoid the use of Enum as well.

Upvotes: 1

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726499

Since you are modeling something that has no logic that can be encoded in an algorithmic way (i.e. there's no algorithm that would figure out that "sod" is liquid and "veg" is not) there is no way around enumerating all related pairs of (item, category) in one way or the other.

There are three approaches to implementing it:

  • Enumerate categories on item's side - this is what your code does in both cases, or
  • Enumerate items on category's side - this would build an enum of categories, and attach a full list of items to each of them, or
  • Enumerate item+category pairs independently - this approach may be useful when storing item/category mapping in the database or in a configuration file.

I would recommend taking the third approach as it is the most "symmetric" one. Make a table for categories with category codes, and add a "cross-table" (or a cross-file) that has all pairs of categories and their corresponding items. Read the cross table/file at startup, and set up the dependencies on both sides.

public Enum ItemType {
    FRUITS("fru")
,   VEGETABLES("veg")
,   LIQUOURS("liq")
,   SODAS("sod");
    public void addCategory(ItemCategory category) ...;
    public EnumSet<ItemCategory> getItemCategories() ...;
}
public Enum ItemCategory {
    LIQUIDS("liq")
,   SNACKS("snk")
,   FAST("fst");
    public void addItem(ItemType type) ...;
    public EnumSet<ItemType> getItemTypes() ...;
}

Cross-file or cross-table may look like this:

liq liq
sod liq
fru snk
fru fst
sod fst

You process it by enumerating pairs, and calling addCategory on the pair's item side, and calling addItem on the pair's category side.

Upvotes: 2

Mario
Mario

Reputation: 1781

I would do something like:

enum Category {
    LIQUID, SOLID;
}

enum ItemType {
    FRUITS("fru", SOLID),
    VEGETABLES("veg", SOLID),
    LIQUOURS("liq", LIQUID),
    SODAS("sod", LIQUID);

    private String dbCode;
    private Category category;
    public ItemType(String dbCode, Category category){
        this.dbCode = dbCode;
        this.category = category;
    }

    /* getters / setters */
}

That would allow, for example, that you can add new products and categories (e.g. BUTANE("but", GAS)) without having to modify the existing code (as would happen in Approach 2).

On the other hand, if the number of categories and items is long and changing, I would consider to use a SQL database.

Upvotes: 3

user180100
user180100

Reputation:

How about using an EnumSet for that?

public enum ItemType
{
    FRUITS("fru"),
    VEGETABLES("veg"),
    LIQUOURS("liq"),
    SODAS("sod");

    public static final EnumSet<ItemType> LIQUIDS = EnumSet.of(LIQUOURS, SODAS);

    // ...
}

Then you can use ItemType.LIQUIDS.contains(someItemType) to check if someItemType is a "liquid".

Upvotes: 5

Related Questions