【问题标题】:Using enum to implement multitons in JavaJava中使用枚举实现multitons
【发布时间】:2014-09-17 23:01:07
【问题描述】:

我想要一个有限的固定目录,包含某个复杂接口的实例。标准的multiton 模式有一些不错的特性,比如惰性实例化。然而,它依赖于一个键,例如一个看起来很容易出错和脆弱的字符串。

我想要一个使用枚举的模式。它们有很多很棒的功能并且很健壮。我试图为此找到一个标准的设计模式,但已经画了一个空白。所以我想出了我自己的,但我对它不是很满意。

我使用的模式如下(界面在这里高度简化以使其可读):

interface Complex { 
    void method();
}

enum ComplexItem implements Complex {
    ITEM1 {
        protected Complex makeInstance() { return new Complex() { ... }
    },
    ITEM2 {
        protected Complex makeInstance() { return new Complex() { ... }
    };

    private Complex instance = null;

    private Complex getInstance() {
        if (instance == null) {
            instance = makeInstance();
        }
        return instance;
    }

    protected void makeInstance() {
    }

    void method {
        getInstance().method();
    }
}

这个模式有一些非常好的特性:

  • 枚举实现了使其使用非常自然的接口:ComplexItem.ITEM1.method();
  • 延迟实例化:如果构建成本很高(我的用例涉及读取文件),则仅在需要时才会发生。

话虽如此,对于这样一个简单的要求,它似乎非常复杂和“hacky”,并且以我不确定语言设计者的意图的方式覆盖枚举方法。

它还有另一个明显的缺点。在我的用例中,我希望接口扩展 Comparable。不幸的是,这与 Comparable 的枚举实现发生冲突,使代码无法编译。

我考虑的另一种选择是使用标准枚举,然后使用一个单独的类将枚举映射到接口的实现(使用标准多点模式)。这有效,但枚举不再实现在我看来不是意图的自然反映的接口。它还将接口的实现与似乎封装不佳的枚举项分开。

另一种选择是让枚举构造函数实现接口(即在上面的模式中删除对“makeInstance”方法的需要)。虽然这有效,但它消除了仅在需要时运行构造函数的优势)。它也不能解决扩展 Comparable 的问题。

所以我的问题是:谁能想到更优雅的方式来做到这一点?

作为对 cme​​ts 的回应,我将尝试指定我尝试首先解决的具体问题,然后通过一个示例来解决。

  • 有一组固定的对象实现给定的接口
  • 对象是无状态的:它们仅用于封装行为
  • 每次执行代码时只会使用对象的一个​​子集(取决于用户输入)
  • 创建这些对象的成本很高:只应在需要时创建一次
  • 对象共享很多行为

这可以通过为每个对象使用单独的类或超类来实现共享行为的单独的单例类来实现。这似乎不必要地复杂。

现在举个例子。系统在一组区域中计算几种不同的税收,每个区域都有自己的税收计算算法。预计区域集永远不会改变,但区域算法会定期改变。特定的区域费率必须在运行时通过缓慢且昂贵的远程服务加载。每次调用系统时,都会给它一组不同的区域进行计算,因此它应该只加载请求的区域的速率。

所以:

interface TaxCalculation {
    float calculateSalesTax(SaleData data);
    float calculateLandTax(LandData data);
    ....
}

enum TaxRegion implements TaxCalculation {
    NORTH, NORTH_EAST, SOUTH, EAST, WEST, CENTRAL .... ;

    private loadRegionalDataFromRemoteServer() { .... }
}

推荐背景阅读:Mixing-in an Enum

【问题讨论】:

  • 我认为我同意您展示的解决方案是骇人听闻且过于复杂的。我认为您应该使用最简单的枚举,然后只使用静态工厂。工厂应该缓存值并在需要时创建新值。
  • 延迟加载、从文件中读取、有限的实例集......对我来说听起来像是一堆单身人士。
  • 枚举应该是不可变的和可序列化的,因此违反它似乎可能会导致问题。
  • @wolfcastle 好吧,它们实际上是不可变的。 (如果同步是固定的。)
  • 我有一个 - 在我看来 - 优雅的 enum 键控 Multiton。我明天会发布它,因为我现在没有副本。

标签: java enums


【解决方案1】:

看起来不错。我会像这样使初始化线程安全:

enum ComplexItem implements Complex {
    ITEM1 {
        protected Complex makeInstance() {
            return new Complex() { public void method() { }};
        }
    },
    ITEM2 {
        protected Complex makeInstance() {
            return new Complex() { public void method() { }}
    };

    private volatile Complex instance;

    private Complex getInstance() {
        if (instance == null) {
            createInstance();
        }
        return instance;
    }

    protected abstract Complex makeInstance();

    protected synchronized void createInstance() {
        if (instance == null) {
            instance = makeInstance();
        }
    }

    public void method() {
        getInstance().method();
    }
}

修饰符 synchronized 仅出现在 createInstance() 方法上,但包装了对 makeInstance() 的调用 - 传达线程安全性,而不会对对 getInstance() 的调用造成瓶颈,并且程序员不必记住添加 synchronized到每个到makeInstance() 实现。

【讨论】:

  • 谢谢。我真的很喜欢使 makeInstance 抽象的想法:它使意图更加清晰。
  • 不应该是== null in createInstance()吗?
  • @biziclop 嗯嗯是的。固定的。谢谢。
【解决方案2】:

这对我有用 - 它是线程安全的和通用的。 enum 必须实现 Creator 接口,但这很容易 - 正如最后的示例用法所示。

此解决方案破坏了您在enum 即存储对象处施加的绑定。这里我只使用enum 作为工厂来创建对象——这样我可以存储任何类型的对象,甚至让每个enum 创建不同类型的对象(这是我的目标)。

这使用ConcurrentMapFutureTask 的线程安全和延迟实例化的通用机制。

在程序的整个生命周期中保持FutureTask 的开销很小,但可以通过一些调整来改善。

/**
 * A Multiton where the keys are an enum and each key can create its own value.
 *
 * The create method of the key enum is guaranteed to only be called once.
 *
 * Probably worth making your Multiton static to avoid duplication.
 *
 * @param <K> - The enum that is the key in the map and also does the creation.
 */
public class Multiton<K extends Enum<K> & Multiton.Creator> {
  // The map to the future.
  private final ConcurrentMap<K, Future<Object>> multitons = new ConcurrentHashMap<K, Future<Object>>();

  // The enums must create
  public interface Creator {
    public abstract Object create();

  }

  // The getter.
  public <V> V get(final K key, Class<V> type) {
    // Has it run yet?
    Future<Object> f = multitons.get(key);
    if (f == null) {
      // No! Make the task that runs it.
      FutureTask<Object> ft = new FutureTask<Object>(
              new Callable() {

                public Object call() throws Exception {
                  // Only do the create when called to do so.
                  return key.create();
                }

              });
      // Only put if not there.
      f = multitons.putIfAbsent(key, ft);
      if (f == null) {
        // We replaced null so we successfully put. We were first!
        f = ft;
        // Initiate the task.
        ft.run();
      }
    }
    try {
      /**
       * If code gets here and hangs due to f.status = 0 (FutureTask.NEW)
       * then you are trying to get from your Multiton in your creator.
       *
       * Cannot check for that without unnecessarily complex code.
       *
       * Perhaps could use get with timeout.
       */
      // Cast here to force the right type.
      return type.cast(f.get());
    } catch (Exception ex) {
      // Hide exceptions without discarding them.
      throw new RuntimeException(ex);
    }
  }

  enum E implements Creator {
    A {

              public String create() {
                return "Face";
              }

            },
    B {

              public Integer create() {
                return 0xFace;
              }

            },
    C {

              public Void create() {
                return null;
              }

            };
  }

  public static void main(String args[]) {
    try {
      Multiton<E> m = new Multiton<E>();
      String face1 = m.get(E.A, String.class);
      Integer face2 = m.get(E.B, Integer.class);
      System.out.println("Face1: " + face1 + " Face2: " + Integer.toHexString(face2));
    } catch (Throwable t) {
      t.printStackTrace(System.err);
    }
  }

}

在 Java 8 中更简单:

public class Multiton<K extends Enum<K> & Multiton.Creator> {

    private final ConcurrentMap<K, Object> multitons = new ConcurrentHashMap<>();

    // The enums must create
    public interface Creator {

        public abstract Object create();

    }

    // The getter.
    public <V> V get(final K key, Class<V> type) {
        return type.cast(multitons.computeIfAbsent(key, k -> k.create()));
    }
}

【讨论】:

    【解决方案3】:

    对这种模式的一个想法是:惰性实例化不是线程安全的。这可能好也可能不好,这取决于您想如何使用它,但值得了解。 (考虑到枚举初始化本身是线程安全的。)

    除此之外,我看不到一个更简单的解决方案,它可以保证完全的实例控制、直观并使用惰性实例化。

    我也不认为这是对枚举方法的滥用,它与 Josh Bloch 的 Effective Java 推荐的将不同策略编码到枚举中的方法没有太大区别。

    【讨论】:

      猜你喜欢
      • 2014-12-04
      • 2011-07-17
      • 2011-08-28
      • 1970-01-01
      • 2010-11-12
      • 1970-01-01
      • 1970-01-01
      • 2018-05-30
      • 2012-01-30
      相关资源
      最近更新 更多