Search code examples
javamethodsenumsfielddry

Code reuse: returning lists of enum fields with common getter methods


I have two enums:

Main Menu Options

public enum MainMenuOptions {
    
    EXIT("Exit"),
    VIEW_RESERVATIONS("View Reservations By Host"),
    CREATE_RESERVATION("Create A Reservation"),
    EDIT_RESERVATION("Edit A Reservation"),
    CANCEL_RESERVATION("Cancel A Reservation");
    
    private final String message;
    
    MainMenuOptions(String message) {
        this.message = message;
    }
    
    public String getMessage() {
        return message;
    }
    
    public static List<String> asListString() {
        return Arrays.stream(MainMenuOptions.values())
                .map(MainMenuOptions::getMessage)
                .collect(Collectors.toList());
    }
}

Host Selection Method Options

public enum HostSelectionMethodOptions {
    
    FIND_ALL("Find all"),
    FIND_BY_LASTNAME_PREFIX("Find by last name prefix"),
    FIND_BY_CITY_STATE("Find by city & state");
    
    String message;
    
    HostSelectionMethod(String message) {
        this.message = message;
    }
    
    public String getMessage() {
        return message;
    }
    
    public static List<String> asListString() {
        return Arrays.stream(HostSelectionMethod.values())
                .map(HostSelectionMethod::getMessage)
                .collect(Collectors.toList());
    }
}

Both enums share the same field

private final String message;

The same getter

public String getMessage() {
    return message;
}

And the same asListString() method

public static List<String> asListString() {
    return Arrays.stream(MainMenuOptions.values())
            .map(MainMenuOptions::getMessage)
            .collect(Collectors.toList());
}

How can I DRY out these enums?

I expect to have more enums with the same fields and methods, and it seems silly to write out the same thing over and over again for each one.

  • I tried making both of the enums extend a superclass, but enums cannot have extends clauses
  • I can create an interface that specifies the contract for the asListString() method, but that doesn't allow me to actually reuse any code.

The flavor I was hoping the code could have is something like this:

public class Utils {
    
    public static List<String> enumAsListString(Enum e) {
        return e.values().stream.map(e::getMessage).collect(Collectors.toList());
    }
}

Solution

  • This is probably one of the cases where you need to pick one between being DRY and using enums.

    Enums don't go very far as far as code reuse is concerned, in Java at least; and the main reason for this is that primary benefits of using enums are reaped in static code - I mean static as in "not dynamic"/"runtime", rather than static :). Although you can "reduce" code duplication, you can hardly do much of that without introducing dependency (yes, that applies to adding a common API/interface, extracting the implementation of asListString to a utility class). And that's still an undesirable trade-off.

    Furthermore, if you must use an enum (for such reasons as built-in support for serialization, database mapping, JSON binding, or, well, because it's data enumeration, etc.), you have no choice but to duplicate method declarations to an extent, even if you can share the implementation: static methods just can't be inherited, and interface methods (of which getMessage would be one) shall need an implementation everywhere. I mean this way of being "DRY" will have many ways of being inelegant.

    If I were you, I would simply make this data completely dynamic

    final class MenuOption {
        private final String category; //MAIN_MENU, HOT_SELECTION
        private final String message; //Exit, View Reservation By Host, etc.
        public static MenuOption of(String key, String message) {
            return new MenuOption(key, message);
        }
    }
    

    This is very scalable, although it introduces the need to validate data where enums would statically prevent bad options, and possibly custom code where an enum would offer built-in support.

    It can be improved with a "category" enum, which gives static access to menu lists, and a single place for asListString():

    enum MenuCategory {
        MAIN_MENU(
            MenuOption.of("Exit"), 
            MenuOption.of("View Reservations By Host")
        ),
        HOT_SELECTION(
            MenuOption.of("Find All")
        );
        
        private final List<MenuOption> menuOptions;
        
        MenuCategory(MenuOption... options) {
            this.menuOptions = List.of(options); //unmodifiable
        }
        
        public List<String>asListString() {
            return this.menuOptions.stream()
                       .map(MenuOption::getMessage)
                       .collect(Collectors.toList());
        }
    }
    

    It should be clear that you can replace class MenuOption with a bunch of enums implementing a common interface, which should change little to nothing in MenuCategory. I wouldn't do that, but it's an option.