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

20

我正在重写一些代码,我决定以枚举的方式来重新创建类,因为有固定数量的工作表。这是基于可读性考虑,相对于伸缩构造函数而言,建造者模式更易读。

这段代码会获取一些 .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 希望我枚举声明('enum 构造函数' 的逗号分隔列表)只包含构造函数,而不是其他额外的方法。我可以将这些方法移动到下面的“构建器”方法中,而不会有任何问题。
public void buildSheets(){
    mySheet1.addSubSheet("pathToSubSheet1.xls");
    mySheet2.addHeaderSheet("pathToHeaders.xls").addSubSheet("pathtoSubSheet2.xls");
}

这是在枚举中实现建造者模式的唯一方法吗?它确实需要我运行一个单独的方法,但不会太麻烦。虽然我觉得我正在打破该模式(如果这样能起作用,那也不是什么坏事)。

注:我已经好好地查看了 SO 或网络上的其他地方,看看是否有人提出过这个问题。我找到的最接近的是此处关于 Enums 和 Factories 的问题,但那并没有完全回答我的问题。此外,我知道这不完全是建造者模式,因为我没有一个单独的类,然后接受一个 build() 方法来创建一个新的枚举。我想这是我初始设计中问题的根源,但我对 Java 相对较新。

那么,在 Java 枚举上使用建造者模式的更好方法存在吗?还是我现有的代码已经足够接近了?

3个回答

30

虽然它不完全符合建造者模式,但简短的答案是可以的。有点类似。

缺失的部分是无法调用.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;
    }
}

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

因此,它并不完全是 Bloch 的构建器,但它非常接近。


这是你之前使用过的东西,还是你写的新代码作为示例? - AncientSwordRage
3
有点两者兼备。我想使用一个建造者,找到了一些答案,但不喜欢它们,于是用这种方式来完成。然后我想在这个问题中分享我的解决方案,以防对某人有所帮助。 - Emerson Farrugia
1
非常感谢!这让我的枚举类更易读了。 - Thomas Vos

7

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

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 所施加的限制,同时保留生成器/流畅模式的简洁性、方便性和灵活性。

这是一个非常聪明的解决方法!我的唯一问题是:我是否需要修改这些方法,并且我是否可以将实例块内容写成 this.methodName(...);,因为我在编写方法时更喜欢显式表达。 - AncientSwordRage
1
你不需要修改这些方法,但我建议你不要使用流畅的风格(返回this),并将它们设为private,除非你需要在类外部访问它们。你可以写this.,但这是多余的,从风格的角度来看并不推荐,特别是因为this的方法从不需要限定;只有字段需要限定,而且只有当它们与参数名冲突时才需要限定,并且实例块没有参数。 - Bohemian
Bohemian,我尝试使用实例块,但包含枚举的类是静态的,将它们放在那里会导致错误。现在我暂时把它们留在原地,但如果我有时间,我会尝试解决这个问题。现在,如果你知道方法应该是什么样子的,能否把它们放到答案中? - AncientSwordRage
在静态类中不应该有任何区别。将完整的代码发布在问题中或者像pastebin这样的地方(并将链接粘贴在此处),我会为您解决它。 - Bohemian
我很想这么做,但由于我的工作地点,不可能实现。我能做的最好的就是从记忆中模拟代码,并尝试找到能够给出同样结果的东西。如果我得到了有用的东西,我会发布出来的。谢谢! - AncientSwordRage
3
Setter 方法无法设置为私有(错误信息提示有点奇怪:错误:(8, 14)java:不能从静态上下文引用非静态方法id(java.lang.String)),但是使用 protected 方法可以实现。谢谢。 - user1067920

1

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

EnumConstant: Annotationsopt Identifier Argumentsopt ClassBodyopt

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

此外,您使用建造者模式构建枚举实例的做法值得商榷,因为通常情况下,建造者模式用于具有大量实例(字段值组合)的情况,与用于少量实例的枚举概念相反。


感谢您的回答dcernahoschi,特别是指向定义。实际上,我有20-25个枚举实例在运行,每个实例都具有略微不同的标题、子表等组合。多少个足够大以证明需要使用构建器,多少个足够少以证明需要使用枚举?随着更多的开发,这可能会改变(类中后来硬编码的标题可能会转换为header.xls表等),因此这种模式确实适合我。 - AncientSwordRage

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接