Can I use the builder pattern on a Java Enum

和自甴很熟 提交于 2020-01-22 08:54:05

问题


I'm re-writing some code, and I've decided the way to recreate the class, as there are a fixed number of sheets, I'm creating them as enums. This is a decision based on the readability of a builder patter vs a telescoping constructor.

The code I'm grabs some some .xls files, adds headers (and reads some from other .xls files) and perhaps some sub-sheets. It then merges a variety of these sheets together in a specific way to make tabs on a main excel workbook. My issue is that some of the workbook tabs take different numbers of sheets are arguments. I'm trying to apply the builder pattern. This is the sort of code I'm trying to write:

public enum workBookSheet {
    mySheet1("Name1","mainSheet1.xls",true,1).addSubSheet("pathToSubSheet1.xls"),
    mySheet2("Name2","mainSheet2.xls",true,2).addHeaderSheet("pathToHeaders.xls").addSubsheet("pathtoSubSheet2.xls");

    private String tabName;
    private String mainSheetName;
    private Boolean available;
    private Integer order;
    private String subSheetName;
    private String headerSheetName;

    private workBookSheet(String tabName, String mainSheetName, Boolean available, Integer order){
        this.tabName = tabName;
        this.mainSheetName = mainSheetName;
        this.available = available;
        this.order = order;
    }
    public workBookSheet addSubSheet(String subSheetName){
        this.subSheetName = subSheetName;
        return this;
    }
    public workBookSheet addHeaderSheet(String headerSheetName){
        this.headerSheetName = headerSheetName;
        return this;
    }

}

The error that java is giving me seems to be saying that Java expects my enum declaration (comma delimited list of 'enum constructors' at the top) to only have the constructor in it, and not additional methods. I can move those methods to a 'builder' method below, without complaint.

public void buildSheets(){
    mySheet1.addSubSheet("pathToSubSheet1.xls");
    mySheet2.addHeaderSheet("pathToHeaders.xls").addSubSheet("pathtoSubSheet2.xls");
}

Is this the only way to implement a builder pattern on an enum? It does require me to run a separate method, which isn't too much hassle. IT does feel like I'm breaking the pattern though (I guess, not such a bad thing if this works.)

N.B I've had a good look around to see if anyone else has asked this question, on SO or elsewhere on the web. The closest I found was a question here on Enums and Factories, but that doesn't quite answer my question. Also I'm aware this isn't quite the builder pattern, as I don't have a separate class that then accepts a build() method that creates a new enum. I guess this is the root of the problem in my initial design, but I am relatively new to Java.

So Is there a better way to use a builder pattern on a Java enum? Or is what I have 'close enough'?


回答1:


Although it doesn't strictly conform to the builder pattern, the short answer is yes. Sort of.

The missing piece is not being able to call .build() to instantiate the enum constant, because build() can't use new. But you can get quite a few of the benefits of the builder pattern. And let's face it, you can't use static factory methods, and inline subclassing of enum constants is weird.

Here's an example using a Country enumeration.

package app;

import org.apache.commons.lang.StringUtils;
import javax.annotation.Nullable;
import java.util.EnumSet;
import java.util.Set;
import static app.Language.*;
import static com.google.common.base.Preconditions.*;

enum Language {
    ITALIAN,
    ENGLISH,
    MALTESE
}

public enum Country {

    ITALY(new Builder(1, "Italy").addLanguage(ITALIAN)),
    MALTA(new Builder(2, "Malta").addLanguages(MALTESE, ENGLISH, ITALIAN).setPopulation(450_000));

    final private int id;
    final private String name;
    final private Integer population;
    final private Set<Language> languages;

    private static class Builder {

        private int id;
        private String name;
        private Integer population;
        private Set<Language> languages = EnumSet.noneOf(Language.class);

        public Builder(int id, String name) {
            checkArgument(!StringUtils.isBlank(name));

            this.id = id;
            this.name = name;
        }

        public Builder setPopulation(int population) {
            checkArgument(population > 0);

            this.population = population;
            return this;
        }

        public Builder addLanguage(Language language) {
            checkNotNull(language);

            this.languages.add(language);
            return this;
        }

        public Builder addLanguages(Language... language) {
            checkNotNull(language);

            this.languages.addAll(languages);
            return this;
        }
    }

    private Country(Builder builder) {

        this.id = builder.id;
        this.name = builder.name;
        this.population = builder.population;
        this.languages = builder.languages;

        checkState(!this.languages.isEmpty());
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Nullable
    public Integer getPopulation() {
        return population;
    }

    public Set<Language> getLanguages() {
        return languages;
    }
}

You can even put static factory methods in the builder if you have common ways to build a constant.

So it's not quite Bloch's builder, but it's pretty close.




回答2:


You can use instance blocks (often incorrectly called "double brace initializers") to customise construction with arbitrary code:

public enum workBookSheet {

    mySheet1("Name1", "mainSheet1.xls", true, 1) {{
        addSubSheet("pathToSubSheet1.xls");
    }},
    mySheet2("Name2", "mainSheet2.xls", true, 2) {{
        // you can use the fluent interface:
        addHeaderSheet("pathToHeaders.xls").addSubSheet("pathtoSubSheet2.xls");
        // but I would prefer coding separate statements:
        addHeaderSheet("pathToHeaders.xls");
        addSubSheet("pathtoSubSheet2.xls");
    }};

    // rest of your class the same...
}

Employing this syntax allows you to work around the limitations imposed by an enum but still have the brevity, convenience and flexibility of a builder/fluent pattern.




回答3:


mySheet1, mySheet2, etc. are enum constants which follows the JLS syntax defined in section 8.9.1

EnumConstant: Annotationsopt Identifier Argumentsopt ClassBodyopt

So, you can follow the enum constant by an argument list (the parameters to pass to the constructor) but you can't call a method on the enum constant while declaring it. At most you can add a class body for it.

Beside this, your usage of builder pattern to build enum instances is questionable as in general the builder pattern is used when you have a large number of instances (combinations of field values) in contrast with the concept of enums used for a few instances.



来源:https://stackoverflow.com/questions/15406717/can-i-use-the-builder-pattern-on-a-java-enum

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!