正确的方法来创建多个全局常量?

时间:2014-09-09 20:54:57

标签: java design-patterns anti-patterns

我在一个(某种旧的)应用程序上工作,其中有一个巨大的全局枚举/类的迷宫,它声明了声明常量键/值集的常量键/类。

这些值中的许多基本上都是冗余的...相同的基本常量在不同的枚举中以略微不同的名称声明,在每一个中它与一组不同的值相关联。

通常,这样的类引用其他类似的巨大类中的值,以创建一些常量层次结构......正如您在下面的示例中所看到的那样。

定义此类常量的正确方法是什么?它们应该像现在一样硬编码吗?

示例:

public class ParameterSet {

    //hundreds of similar declarations..........

    public static ParameterTypeAbstract BREADCRUMB1_BREADCRUMB2_BREADCRUMB3_BREADCRUMB4_452 = new ParameterTypeSet(
            KeywordType.PARAMETER_OF_SOME_TYPE_26659,
            ParameterGroupType.PARAMETER_OF_SOME_OTHER_TYPE_967347,
            ParameterScopeType.ACCOUNT_TYPE_557.ACCOUNT_SUB_TYPE_33791.getStringCode(), ParameterScopeImportantType.getStringCodes(),
            "Some description",
            true, true, true, null, null);

    public static ParameterTypeAbstract BREADCRUMB1_BREADCRUMB2_BREADCRUMB3_BREADCRUMB4_453 = new ParameterTypeSet(
            KeywordType.PARAMETER_OF_SOME_TYPE_90689,
            ParameterGroupType.PARAMETER_OF_SOME_OTHER_TYPE_867335,
            ParameterScopeType.ACCOUNT_TYPE_538.ACCOUNT_SUB_TYPE_48224.getStringCode(), ParameterScopeImportantType.getStringCodes(),
            "Some other description",
            true, true, true, null, null);

    //hundreds of similar declarations..........
}

1 个答案:

答案 0 :(得分:0)

以下是我过去使用的一些策略。如果我知道你要解决的实际问题是坦率的话,我会更容易回答,我对BREADCRUMB1_BREADCRUMB2_BREADCRUMB3_BREADCRUMB4_452真的很有道理。为了给出一个说明性的答案,我将假设以下示例:假设我们想要做一些图形并使用调色板。我们想要几个调色板(例如TangoGnome等),每个调色板都有一组有限的命名颜色。我们的要求是:

  • 为嵌套的常量结构提供一致的接口。
  • 提供静态类型安全的方式来引用调色板中的颜色。
  • 提供一种方便(并且最有效)的方式来按名称查找(在运行时)调色板和颜色。
  • 提供一种方便(且最有效)的方式来枚举(在运行时)所有调色板和颜色。
  • 首选外部配置来源。

我们希望通过具有构造函数的不可变Color类的实例来表示颜色,该构造函数将打包的RGB值作为参数。

具有公共静态常量的嵌套类

public final class Palettes {

    public static final class Tango {

        public static final Color BUTTER_LIGHT = new Color(0xFCE94F);
        public static final Color BUTTER_MEDIUM = new Color(0xEDD400);
        public static final Color BUTTER_DARK = new Color(0xC4A000);
        public static final Color ORANGE_LIGHT = new Color(0xFCAF3E);
        public static final Color ORANGE_MEDIUM = new Color(0xF57900);
        // Many more Tango colors...

        private Tango() { throw new Error("cannot be instantiated"); }
    }

    public static final class Gnome {

        public static final Color BASIC_3D_HIGHLIGHT = new Color(0xEAE8E3);
        public static final Color BASIC_3D_DARK = new Color(0x807D74);
        public static final Color GREEN_HIGHLIGHT = new Color(0xC5D2C8);
        // Many more Gnome colors...

        private Gnome() { throw new Error("cannot be instantiated"); }
    }

    // Many more color palettes...

    private Palettes() { throw new Error("cannot be instantiated"); }
}

这些类很简单,可以很容易地在构建过程中生成。就个人而言,我喜欢将这些数据保存在XML文件中,并使用XSLT样式表从它们生成Java(或其他)代码。我已经“禁用”了类的构造函数并将它们声明为final,因为只有静态成员的类既不应该实例也不应该是子类。

让我们看看这个解决方案符合我们要求的程度。

如果我们想在调色板中引用一种颜色并在编译时知道我们想要哪一种颜色,我们就可以这样做:

Color color1 = Palettes.Tango.ORANGE_LIGHT;

这给我们静态类型检查;如果我们不小心引用了不存在的颜色或调色板或拼错了名称,编译器会告诉我们。

Color color2 = Palettes.Gonme.GREEN_HIGHLIGHT;  // compile-time error
Color color3 = Palettes.Gnome.GREEN_HIHGLIGHT;  // compile-time error

如果我们从运行时获得用户的调色板和颜色名称并想要查找它,该怎么办?没有方便的方法。毫无疑问,即使可以从与类相同的数据源轻松生成代码,以下解决方案也是可怕的。

public Color selectColor(final String palette, final String color) {
    switch (palette) {
    case "Tango":
        switch (color) {
        case "Butter Light":       return Palettes.Tango.BUTTER_LIGHT;
        case "Butter Medium":      return Palettes.Tango.BUTTER_MEDIUM;
        case "Butter Dark":        return Palettes.Tango.BUTTER_DARK;
        case "Orange Light":       return Palettes.Tango.ORANGE_LIGHT;
        case "Orange Medium":      return Palettes.Tango.ORANGE_MEDIUM;
        // case ...
        default: throw new IllegalArgumentException(String.format(
            "There is no color '%s' in palette '%s'", color, palette));
        }
    case "Gnome":
        switch (color) {
        case "Basic 3D Highlight": return Palettes.Gnome.BASIC_3D_HIGHLIGHT;
        case "Basic 3D Dark":      return Palettes.Gnome.BASIC_3D_DARK;
        case "Green Highlight":    return Palettes.Gnome.GREEN_HIGHLIGHT;
        // case ...
        default: throw new IllegalArgumentException(String.format(
            "There is no color '%s' in palette '%s'", color, palette));
        }
    // case ...
    default:
        throw new IllegalArgumentException(String.format(
            "There is no palette '%s'", palette));
    }
}

另一种选择是使用反射,这可能更好,但仍然是设计选择不佳的指标。

枚举所有调色板/颜色同样如此。

嵌套词典

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

final class GlobalConstants {

    public static final Map<String, Map<String, Color>> COLOR_PALETTES = GlobalConstants.makeColorPalettes();

    private static Map<String, Map<String, Color>> makeColorPalettes() {
        final Map<String, Map<String, Color>> colorPalettes = new HashMap<String, Map<String, Color>>();
        // These could be loaded at class load time from an external
        // configuration.
        {
            final Map<String, Color> palette = new HashMap<String, Color>();
            palette.put("Butter Light",       new Color(0xFCE94F));
            palette.put("Butter Medium",      new Color(0xFCE94F));
            palette.put("Butter Dark",        new Color(0xFCE94F));
            palette.put("Orange Light",       new Color(0xFCE94F));
            palette.put("Orange Medium",      new Color(0xFCE94F));
            // Many more Tango colors...
            colorPalettes.put("Tango", Collections.unmodifiableMap(palette));
        }
        {
            final Map<String, Color> palette = new HashMap<String, Color>();
            palette.put("Basic 3D Highlight", new Color(0xEAE8E3));
            palette.put("Basic 3D Dark",      new Color(0x807D74));
            palette.put("Green Highlight",    new Color(0xC5D2C8));
            // Many more Gnome colors...
            colorPalettes.put("Gnome", Collections.unmodifiableMap(palette));
        }
        // Many more color palettes...
        return Collections.unmodifiableMap(colorPalettes);
    }

    private GlobalConstants() { throw new Error("cannot be instantiated"); }
}

我选择java.util.Map更像是占位符而不是建议。人们应该定义一个专门的ColorPalette类,它具有更具描述性的名称,可以提供所需的方法。

一旦我们决定将常量放入在运行时汇编的字典中,我们就不再需要使用代码生成来将常量硬编码到Java源代码中。如果我们想要(生成一个显示的文件),我们仍然可以这样做,但我们同样可以从属性文件中读取信息,查询数据库或其他任何内容。

当然,这是以丢失静态类型检查为代价的。我们仍然可以方便地在任何调色板中引用任何颜色:

Color color1 = GlobalConstants.COLOR_PALETTES.get("Tango").get("Orange Light");

但是,这些现在将是运行时错误:

Color color2 = GlobalConstants.COLOR_PALETTES.get("Gonme").get("Green Highlight");  // NullPointerException
Color color3 = GlobalConstants.COLOR_PALETTES.get("Gnome").get("Green Hihglight");  // color3 == null, surprise will come later

在运行时查找调色板和颜色现在变得微不足道了,因为我们的编写时间常量与编译时常量相同。

public Color selectColor(final String palette, final String color) {
    final Map<String, Color> thePalette;
    final Color theColor;
    thePalette = GlobalConstants.COLOR_PALETTES.get(palette);
    if (thePalette == null)
        throw new IllegalArgumentException(String.format(
            "There is no palette '%s'", palette));
    theColor = thePalette.get(color);
    if (theColor == null)
        throw new IllegalArgumentException(String.format(
            "There is no color '%s' in palette '%s'", color, palette));
    return theColor;
}

此外,枚举调色板中的所有调色板或所有颜色只是“for each”循环的一个问题,并且非常有效。

枚举类

最后,我们将看看Java曾经如此强大的enum

enum TangoPalette {

    BUTTER_LIGHT(new Color(0xFCE94F)),
    BUTTER_MEDIUM(new Color(0xEDD400)),
    BUTTER_DARK(new Color(0xC4A000)),
    ORANGE_LIGHT(new Color(0xFCAF3E)),
    ORANGE_MEDIUM(new Color(0xF57900));
    // Many more Tango colors...

    private final Color color;

    TangoPalette(final Color color) {
        this.color = color;
    }

    public Color asColor() {
        return this.color;
    }
}

enum GnomePalette {

    BASIC_3D_HIGHLIGHT(new Color(0xEAE8E3)),
    BASIC_3D_DARK(new Color(0x807D74)),
    GREEN_HIGHLIGHT(new Color(0xC5D2C8));
    // Many more Gnome colors...

    private final Color color;

    GnomePalette(final Color color) {
        this.color = color;
    }

    public Color asColor() {
        return this.color;
    }
}

// Many more palettes...

这又是代码生成的候选者。可能最有可能的是,我们必须重复将Color每个调色板中的enum相关联的逻辑。在这个例子中它看起来并不那么糟糕,但是一旦我们想要比简单的RBG值更多的数据,可能会变得更糟,可能还有额外的便利方法。

否则,这个解决方案看起来几乎是完美的,除了一个主要缺点:我们无法嵌套enum。 (至少,我不知道如何做到这一点。)这意味着它实际上是上述两种方法的混合。

以下是我们在编译时引用常量的方法:

Color color1 = TangoPalette.ORANGE_LIGHT.asColor();
Color color2 = GonmePalette.GREEN_HIGHLIGHT.asColor();  // compile-time error
Color color3 = GnomePalette.GREEN_HIHGLIGHT.asColor();  // compile-time error

这再次从完全静态类型检查中获利。必须写.asColor()并不好,但在我看来只是一个小麻烦。

在运行时查找颜色的便利性介于上述两种方法之间。我们仍然需要编写自己的逻辑来选择调色板,但是一旦我们拥有它,我们就可以使用valueOf方法。

public Color selectColor(final String palette, final String color) {
    final String mangledColor = color.replace(' ', '_').toUpperCase();
    switch (palette) {
    case "Tango":
        return TangoPalette.valueOf(mangledColor).asColor();
    case "Gnome":
        return GnomePalette.valueOf(mangledColor).asColor();
    // case ...
    default:
        throw new IllegalArgumentException(String.format(
            "There is no palette '%s'", palette));
    }
}

同样,枚举给定调色板中的颜色只需使用values方法。

如果我们只能使用所有调色板的名称再添加enum

结论

我喜欢enum,所以最后的解决方案对我最有吸引力。一般来说,我使用以下经验法则:

  • 如果需要递归嵌套,请使用带有公共静态常量的嵌套类的第一种方法。
  • 如果将来从外部数据库加载值似乎并不难以置信,请考虑使用词典。
  • 但是,如果为了静态类型检查而在编译时已经知道大多数值,请远离字典。
  • 如果您只需要一个级别的递归,请使用enum