0

我正在寻找一段代码,它的行为有点像单例但不是(因为单例很糟糕:) 我正在寻找的必须满足这些目标:

  1. 线程安全
  2. 简单(理解和使用,即几行代码。库调用是可以的)
  3. 快速地
  4. 不是单身;对于测试,必须可以覆盖该值(并在测试后将其重置)。
  5. 本地(所有必要信息必须在一个地方)
  6. 惰性(仅在实际需要该值时运行)。
  7. 运行一次(RHS 上的代码必须执行一次且仅一次)

示例代码:

private int i = runOnce(5); // Set i to 5
// Create the connection once and cache the result
private Connection db = runOnce(createDBConnection("DB_NAME"));

public void m() {
    String greet = runOnce("World");
    System.out.println("Hello, "+greet+"!");
}

请注意,这些字段不是静态的;只有表达式的 RHS(右手边)是……在某种程度上是“静态的”。测试应该能够临时注入新igreet

另请注意,这段代码概述了我打算如何使用这个新代码。随意用任何东西替换 runOnce() 或将其移动到其他地方(构造函数,也许,或 init() 方法或 getter)。但是 LOC 越少越好。

一些背景资料:

我不是在寻找 Spring,而是在寻找一段可用于最常见情况的代码:您需要实现一个接口,除了您想要的测试之外,永远不会有第二个实现传入模拟对象。此外,Spring 失败了 #2、#3 和 #5:您需要学习配置语言,您必须在某处设置应用程序上下文,它需要 XML 解析器并且它不是本地的(信息遍布各处)。

由于#5,全局配置对象或工厂不符合要求。

static final因为#4而出局(不能改变决赛)。static由于类加载器问题而有异味,但您可能需要在里面使用它runOnce()。我只是希望能够在表达式的 LHS 中避免它。

一种可能的解决方案可能是将ehcache与默认设置一起使用,该设置将返回相同的对象。由于我可以将内容放入缓存中,因此这也允许随时覆盖该值。但也许有比 ehcache 更紧凑/简单的解决方案(它再次需要 XML 配置文件等)。

[编辑] 我想知道为什么这么多人不赞成这个。这是一个有效的问题,用例很常见(至少在我的代码中)。因此,如果您不理解问题(或其背后的原因),或者您没有答案或不在乎,为什么要投反对票?:/

[EDIT2] 如果您查看 Spring 的应用程序上下文,您会发现超过 99% 的所有 bean 都只有一个实现。你可以拥有更多,但在实践中,你根本没有。因此,我没有分离接口、实现和配置,而是在寻找只有一个实现(在最简单的情况下)、一个 current() 方法和一两行聪明的代码来初始化 current() 的结果的东西一次(第一次调用时)但同时允许覆盖结果(如果可能,线程安全)。把它想象成一个原子的“if(o==null) o = new O(); return o”,你可以在其中覆盖 o。也许 AtomicRunOnceReference 类是解决方案。

现在,我只是觉得我们每天都拥有和使用的并不是最佳的,有一个莫名其妙的解决方案会让我们都拍脑袋说“就是这样”。就像我们几年前 Spring 出现时的感觉一样,我们意识到我们所有的单例问题来自哪里以及如何解决它们。

4

4 回答 4

6

线程安全初始化代码(imho)的最佳习惯用法是惰性内部类。经典版本是

class Outer {
  class Inner {
    private final static SomeInterface SINGLETON;

    static {
      // create the SINGLETON
    }
  }

  public SomeInterface getMyObject() {
    return Inner.SINGLETON;
  }
}

因为它是线程安全的、延迟加载和(恕我直言)优雅的。

现在您需要可测试性和可替换性。在不知道它到底是什么的情况下很难给出建议,但最明显的解决方案是使用依赖注入,特别是如果您使用的是 Spring 并且无论如何都有应用程序上下文。

这样,您的“单例”行为由一个接口表示,您只需将其中一个注入您的相关类(或生产一个工厂),然后您当然可以用您喜欢的任何东西替换它以进行测试。

于 2009-01-14T11:06:03.203 回答
3

这是一个满足我所有要求的解决方案:

/** Lazy initialization of a field value based on the (correct)
* double checked locking idiom by Joschua Bloch
*
* <p>See "Effective Java, Second Edition", p. 283
*/
public abstract class LazyInit<T>
{
    private volatile T field;

    /** Return the value.
    *
    *  <p>If the value is still <code>null</code>, the method will block and
    *  invoke <code>computeValue()</code>. Calls from other threads will wait
    *  until the call from the first thread will complete.
    */
    @edu.umd.cs.findbugs.annotations.SuppressWarnings("UG_SYNC_SET_UNSYNC_GET")
    public T get ()
    {
        T result = field;
        if (result == null) // First check (no locking)
        {
            synchronized (this)
            {
                result = field;
                if (result == null) // Second check (with locking)
                {
                    field = result = computeValue ();
                }
            }
        }
        return result;
    }

    protected abstract T computeValue ();

    /** Setter for tests */
    public synchronized void set (T value)
    {
        field = value;
    }

    public boolean hasValue()
    {
        return field != null;
    }
}
于 2011-03-17T16:46:39.977 回答
1

即使您不使用 IoC 框架(Spring/Guice/...),您也可以使用 IoC 技术。在我看来,这是避免单例的唯一干净方法。

于 2009-01-14T13:39:01.790 回答
0

我认为您可以使用的一种解决方案是提供一种可以在测试中覆盖的受保护方法(我以前用于测试遗留代码的解决方案)。

所以像:

private SomeObject object;

protected SomeObject getObject() {
   if (object == null) {
       object = new SomeObject();
   }
   return object;
}

然后在您的测试课中,您可以执行以下操作:

public void setUp() {
   MyClassUnderTest cut = new MyClassUserTest() {
      @Override
      protected SomeObject getObject() }
         return mockSomeObject;
      }
   };
}

我不得不说我并不太热衷于这种模式,因为它暴露了你可能并不真正想要的受保护字段,但它对于让你摆脱某些无法选择注入的情况很有用

于 2009-01-14T11:09:53.790 回答