Java 基于枚举的单例实现和单元测试,将状态保留为枚举的副作用

Java 基于枚举的单例实现和单元测试,将状态保留为枚举的副作用,java,junit,enums,singleton,Java,Junit,Enums,Singleton,正如许多人所说,单身是不好的,它是一种“反模式”。不管幸运与否,我使用的一些代码确实有它,而且在不久的将来它不会消失。所以我的问题如下 有一个状态的独生子女: import java.util.HashMap; import java.util.Map; public enum Singleton { INSTANCE; private final Map<String, String> _mapper = new HashMap<String, Strin

正如许多人所说,单身是不好的,它是一种“反模式”。不管幸运与否,我使用的一些代码确实有它,而且在不久的将来它不会消失。所以我的问题如下

有一个状态的独生子女:

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

public enum Singleton {
    INSTANCE;

    private final Map<String, String> _mapper = new HashMap<String, String>();

    public String getValue(String key) {
        return _mapper.get(key);
    }

    public void addEntry(String key, String val) {
        _mapper.put(key, val);
    }

    public String removeKey(String key) {
        return _mapper.remove(key);
    }
}
如何在两次测试运行之间清除状态?这可以通过青少年跑步者来完成吗


对象的实际状态更复杂,并通过构造函数初始化。我一直在寻找类似这样的东西:

您可以使用一个在执行每个测试后都将执行的方法,该方法在执行之后使用
@After
。在此方法中,可以清除枚举中映射的状态:

@After
public void clear() {
    Singleton.INSTANCE.removeKey(KEY_1);
    Singleton.INSTANCE.removeKey("key_2");
    //and on and on...
}
当然,更好的方法是在
enum
中使用
clear
方法来清除映射的值,因此
@After
方法的主体将变得更短:

@After
public void clear() {
    //assuming you can create such method
    Singleton.INSTANCE.clearEntries();
}

一个选项是在枚举上包含一个包private
clear
方法。这并不是完全的傻瓜式,但这种方法应该足够简单,不会有任何错误。在您的示例中,它只需调用
\u mapper.clear()

另一个选项是使用与
Singleton
相同的方法创建一个类
Delegate
,并让
Singleton
仅委托给该类的一个实例。然后,测试
Delegate
(而不是
Singleton
),每次实例化一个新的

public enum Singleton {
    final Delegate delegate = new Delegate();

    public String getValue(String key) {
        return delegate.getValue(key);
    }

    // etc
}

class Delegate { // probably best as package-private
    private final Map<String, String> _mapper = new HashMap<String, String>();

    public String getValue(String key) {
        return _mapper.get(key);
    }

    // etc
}
公共枚举单例{
最终委托=新委托();
公共字符串getValue(字符串键){
返回delegate.getValue(键);
}
//等
}
类委托{//可能最好作为包私有
私有最终映射_mapper=new HashMap();
公共字符串getValue(字符串键){
返回_mapper.get(键);
}
//等
}
这将为您提供一个具有非全局状态的
委托
类,该类更易于测试。
Singleton
类中的全局状态是它在枚举的类加载时创建的全局
Delegate
实例


当然,测试使用<代码> Stutelon < /C> >的任何类都将是棘手的,但这似乎不是你的问题。

这里有一些你可能要考虑的问题。

  enum Singleton {
    INSTANCE;

    ThreadLocal<Map<String, String>> context = new ThreadLocal<Map<String, String>>();
    private final Map<String, String> _mapper = new HashMap<String, String>();

    void createContext() {
      context.set(new HashMap<String, String>());
    }

    Map<String, String> getMapper() {
      Map<String, String> mapper;
      mapper = context.get();
      if (mapper==null) {
        mapper = _mapper;
      }
      return mapper;
    }

    public String getValue(String key) {
      return getMapper().get(key);
    }
    // ...
  }
enum单例{
实例;
ThreadLocal上下文=新的ThreadLocal();
私有最终映射_mapper=new HashMap();
void createContext(){
set(新的HashMap());
}
映射getMapper(){
地图制图器;
mapper=context.get();
if(映射器==null){
映射器=_映射器;
}
返回映射器;
}
公共字符串getValue(字符串键){
返回getMapper().get(键);
}
// ...
}
在测试中,您将在设置过程中调用
INSTANCE.createContext()


如果从未调用
createContext
,则不会创建线程局部变量,因此使用普通的
映射器。否则,将检索并使用线程局部变量。

clear()将是故事的一部分,因为实际状态更复杂,只有一个片段足够简单,可以重现我的问题。我在回答的第二部分进行了扩展,这听起来对你来说可能是更好的选择。我们将讨论单实例持有者模式。在这种情况下,我知道一个清理状态的解决方法。因为首先使用enum是没有用的。我可以拿起holder解决方案,用反射来清理它。或者只是使用每个测试用例创建的holder实例。但我认为您不需要枚举?我从你的问题中了解到,你不喜欢单身,但你却被它束缚住了(我猜这太痛苦了,不可能把它撕下来,传给所有需要它的人?)。如果这是真的,那么一个解决方案在测试时去掉枚举,但保留它用于遗留代码,有什么不对呢?clear()将是故事的一部分,因为实际状态更复杂,什么是只有一个足够简单的片段来重现我的问题。@ArtemOboturov发布真实的代码以获得更准确的答案。真正的问题是:为什么要在测试之间清除状态?假设我要启动一个paraller runner,您的方法将失败。@ArtemOboturov您首先问如何清除测试运行之间的状态,现在问为什么要这样做?说真的,我还是不明白你真正的问题。此外,如果你不使用我的方法启动一个并行的跑步者,那么测试也会失败。@阿特莫博图洛夫,为什么你不认为它是一个解决方案?这种方法的确切问题是什么?如果Singleton中上下文敏感的成员函数很少,您可以尝试使用
ThreadLocal
变量。如果设置了变量,请使用
ThreadLocal
上下文,否则请使用全局上下文。我认为您需要对问题进行编辑,以便更准确地了解哪些类型的解决方案是可接受的,哪些不可接受,以及为什么不可接受。很难猜测您似乎没有告诉我们的约束。那么测试将测试什么呢?我想是两个不同的实现。除了映射之外,实现是相同的。除了如何检索映射器之外,实现完全相同。如果您设置了一个上下文,则实例将使用该上下文,但在其他情况下不会有任何更改。如果您在测试之间清除它,您将使用不同的映射。@ArtemOboturov理论上您可以调用INSTANCE.createContext()在应用程序的主线程中,测试完全相同的实现,但会丢失singleton的惰性初始化属性。
  enum Singleton {
    INSTANCE;

    ThreadLocal<Map<String, String>> context = new ThreadLocal<Map<String, String>>();
    private final Map<String, String> _mapper = new HashMap<String, String>();

    void createContext() {
      context.set(new HashMap<String, String>());
    }

    Map<String, String> getMapper() {
      Map<String, String> mapper;
      mapper = context.get();
      if (mapper==null) {
        mapper = _mapper;
      }
      return mapper;
    }

    public String getValue(String key) {
      return getMapper().get(key);
    }
    // ...
  }