如何实现近似单例模式?

5
我有时需要创建一些类,这些类在应用程序的生命周期中只能被实例化一次。将它们设置为单例会导致单元测试变得困难。
但是,因为在应用程序的生命周期中应该只有一个这样的对象实例存在,所以当应用程序运行时两次实例化此类对象将是一个错误。
因此,我希望我的应用程序在检测到此类对象在其生命周期内被实例化两次时立即抛出异常,同时允许在单元测试期间多次实例化此类对象。
我认为这不是一个不合理的要求:如果在应用程序的一个生命周期中只应该创建一个这样的对象,那么抛出异常似乎是正确的做法。
以下是我的做法:
/**
 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
 * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
 * document are to be interpreted as described in RFC 2119.
 *
 * You MUST NOT instantiate this class more than once during the application's
 * lifecycle.  If you try to do so, an exception SHALL be thrown.
 *
 * You SHOULD be able to instantiate this class more than once when unit
 * testing.
 *
 * 
 */
public class LifeCycle {

    private static final AtomicInteger cnt = new AtomicInteger( 0 );

    @NotNull
    public static LifeCycle getNewLifeCycle() {
        if ( cnt.incrementAndGet() > 1 && App.isRealApp() ) {
            throw new IllegalStateException("Class is already instantiated");
        }
        return new LifeCycle();
    }

}

当我进行单元测试时,App.isRealApp() 应始终返回 false,而在真正的应用程序运行时始终返回 true

我的问题很简单:这是否有意义,我该如何实现?

3个回答

9
如果您的设计需要单例模式,那么最好使用单例模式而不是尝试复杂的东西。
例如,如果您在单元测试单例时遇到了获取实例以进行模拟的问题,那么我会创建一个可实例化的轻量级代理,该代理提供与您的单例相同的接口。
代理不应具有任何逻辑 - 它应该只是将调用映射到单例。
然后您可以在测试中使用它,并保留代码库中的单例。轻量级代理可以保留为测试套件的一部分,而不必发布。

它适用于我 - 这意味着我可以保持代码库的原样。 - Tim Barrass

5

一种解决方案是使用依赖注入框架,如Spring。

应用程序配置将指定单例实例,但您的测试用例仍然可以创建所需的实例并手动注入它们。

更多信息:http://static.springsource.org/spring/docs/2.5.x/reference/testing.html

另一种选择,如果您的代码中已经散布着静态单例引用,是将Singleton.getInstance()方法转换为真正的工厂,使用系统属性来控制其行为。这个系统属性可以简单地作为一个标志来指示正在进行测试,也可以像JDK使用DocumentBuilderFactory那样使用另一个类的名称作为真正的工厂。


@Anon:+1,我也觉得很有趣,Spring可以在应用程序级别进行配置,只允许单例,同时仍然允许单元测试。很酷,我会继续阅读的。 - NoozNooz42
一个像Spring这样复杂的框架难道不是对于如此简单的问题过于复杂了吗?我认为答案非常明显。你应该编写测试代码,以便测试代码而不是更改代码使其可测试。 - RoflcoptrException
Spring可能有点过于复杂,但是一个控制实例创建的工厂对于这个目的基本上是相同的东西,并且更加轻量级,只要重构负担不太高。 - Tim Barrass
@Anon:是的,我认为你应该测试你的代码,但我认为你不应该仅出于测试目的更改生产代码中的任何内容。 - RoflcoptrException
@Roflcoptr:但这并不完全正确:如果您的代码不能轻松地进行单元测试,那么它可能是非常糟糕的代码。 - NoozNooz42
显示剩余2条评论

0
因此,我希望我的应用程序在其生命周期内一旦检测到实例化了两次这样的对象,就会立即抛出异常。
这就是单例模式所强制执行的。
同时,还允许在单元测试期间多次实例化这样的对象。
你可以使用@com.dp4j.Singleton来实现这一点。
这就是反射API所允许的。
你可以直接使用反射API,或者让dp4j为你注入它。在这里你将找到两种方法的代码。

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