我可以在 Java 枚举上使用构建器模式吗

Can I use the builder pattern on a Java Enum

提问人:AncientSwordRage 提问时间:3/14/2013 更新时间:5/30/2013 访问量:12822

问:

我正在重写一些代码,并且我已经决定了重新创建类的方法,因为有固定数量的工作表,我将它们创建为枚举。这是基于构建器模式与伸缩构造器的可读性的决定。

我正在的代码抓取了一些.xls文件,添加了标题(并从其他.xls文件中读取了一些标题),也许还有一些子表。然后,它以特定方式将这些工作表中的各种合并在一起,以在主 excel 工作簿上制作选项卡。我的问题是某些工作簿选项卡采用不同数量的工作表作为参数。我正在尝试应用构建器模式。这是我尝试编写的代码:

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;
    }

}

java 给我的错误似乎是说 Java 期望我的枚举声明(顶部的“枚举构造函数”的逗号分隔列表)只有构造函数,而不是其他方法。我可以毫无怨言地将这些方法移动到下面的“构建器”方法中。

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

这是在枚举上实现构建器模式的唯一方法吗?它确实需要我运行一个单独的方法,这并不麻烦。不过,它确实觉得我正在打破这种模式(我想,如果这可行,这并不是一件坏事。

注意:我仔细环顾四周,看看是否有其他人在 SO 或网络上的其他地方问过这个问题。我发现的最接近的是关于枚举和工厂的问题,但这并不能完全回答我的问题。我也知道这并不完全是构建器模式,因为我没有一个单独的类,然后接受创建一个新枚举的 build() 方法。我想这是我最初设计中问题的根源,但我对 Java 相对较新。

那么,有没有更好的方法可以在 Java 枚举上使用构建器模式呢?还是我所拥有的“足够接近”?

Java 枚举 生成器

评论


答:

1赞 dcernahoschi 3/14/2013 #1

mySheet1, mySheet2等是枚举常量,遵循第 8.9.1 节中定义的 JLS 语法

枚举常量: 注解opt 标识符 参数opt 类体opt

因此,您可以在枚举常量后面加上参数列表(要传递给构造函数的参数),但不能在声明枚举常量时调用该方法。您最多可以为其添加一个类主体。

除此之外,您使用构建器模式来构建枚举实例是有问题的,因为通常,当您拥有大量实例(字段值的组合)时,会使用构建器模式,而使用用于少数实例的枚举概念。

评论

0赞 AncientSwordRage 3/14/2013
感谢您的回答 dcernahoschi,特别是您为我指出了定义。实际上,我实际上有 20-25 个枚举实例在起作用,每个实例都有微妙不同的标题、子表等组合。有多少“大”到足以证明一个构造器是合理的,有多少“少”到足以证明一个枚举的合理性?随着更多的发展,这可能会改变(类中稍后的硬编码标题可能会转换为标题.xls表等),所以这种模式确实适合我。
7赞 Bohemian 3/14/2013 #2

您可以使用实例块(通常错误地称为“双大括号初始值设定项”)来自定义具有任意代码的构造:

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...
}

使用这种语法可以绕过构建器/流畅模式施加的限制,但仍然具有构建器/流畅模式的简洁性、便利性和灵活性。enum

评论

0赞 AncientSwordRage 3/14/2013
这是一个非常聪明的解决方法!我唯一的问题是:我是否必须修改方法,我可以将实例块内容写成 ,因为我更喜欢在编写方法时明确。this.methodName(...);
1赞 Bohemian 3/14/2013
不需要修改方法,但是我不会打扰流畅的风格(返回),我会这样做,除非你需要它们在你的类之外可见。你可以写,但从风格的角度来看,它是多余的,不建议这样做,特别是因为方法永远不需要限定;只有字段需要限定,并且仅当它们与参数名称冲突时才需要限定,并且实例块没有参数。thisprivatethis.this
0赞 AncientSwordRage 3/15/2013
波西米亚语,我尝试使用实例块,但是枚举所在的类是静态的,将它们放在适当的位置意味着它们会出错。我现在让它们保持原样,但如果我有时间,我会尝试解决它。现在,如果你知道这些方法应该是什么样子,你能把它们放到答案中吗?
0赞 Bohemian 3/17/2013
在静态类中应该没有区别。将您的完整代码发布在问题中或像 pastebin 这样的地方(并将链接粘贴到此处),我会让它为您工作。
3赞 user1067920 2/9/2016
setter 不能是私有的(奇怪的错误消息:),但是使用受保护的方法,这是有效的。咔嚓Error:(8, 14) java: non-static method id(java.lang.String) cannot be referenced from a static context
30赞 Emerson Farrugia 5/30/2013 #3

虽然它并不严格符合构建器模式,但简短的回答是肯定的。算是吧。

缺少的部分是无法调用来实例化枚举常量,因为 build() 不能使用 .但是,您可以获得构建器模式的很多好处。让我们面对现实吧,你不能使用静态工厂方法,枚举常量的内联子类化很奇怪。.build()new

下面是使用 Country 枚举的示例。

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;
    }
}

如果您有构建常量的常用方法,您甚至可以将静态工厂方法放在构建器中。

所以它不完全是布洛赫的建造者,但它非常接近。

评论

0赞 AncientSwordRage 6/3/2013
这是您以前使用过的东西,还是您编写的新代码作为示例?
3赞 Emerson Farrugia 6/5/2013
两者兼而有之。我想使用构建器,找到了一些答案,但不喜欢它们,并想出了这样做的方法。然后我想我会在这个问题上分享我的解决方案,以防它对某人有帮助。
1赞 Thomas Vos 5/26/2017
非常感谢!这使我的枚举类更具可读性。