Способы ленивой инициализации "запускай один раз" в Java с переопределением из модульных тестов - PullRequest
0 голосов
/ 14 января 2009

Я ищу кусок кода, который ведет себя немного как синглтон, но это не так (потому что синглтоны плохи :) То, что я ищу, должно соответствовать следующим целям:

  1. Потокобезопасный
  2. Простой (Понять и использовать, т. Е. Несколько строк кода. Библиотечные вызовы в порядке)
  3. Fast
  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+"!");
}

Обратите внимание, что поля не static; только RHS (правая часть) выражения ... в некоторой степени "статичен". Тест должен иметь возможность вводить новые значения для i и greet временно.

Также обратите внимание, что этот фрагмент кода описывает, как я собираюсь использовать этот новый код. Не стесняйтесь заменить runOnce () на что-нибудь или переместить его в другое место (может быть, конструктор, метод init () или метод получения). Но чем меньше LOC, тем лучше.

Некоторая справочная информация:

Я не ищу Spring, я ищу кусок кода, который можно использовать для наиболее распространенного случая: вам нужно реализовать интерфейс, и второй реализации не будет, за исключением тестов, в которых Вы хотите передать в фиктивные объекты. Кроме того, Spring терпит неудачу # 2, # 3 и # 5: вам нужно выучить язык конфигурации, вы должны где-то настроить контекст приложения, ему нужен анализатор XML, и он не локальный (информация распространяется повсеместно).

Глобальный объект конфигурации или фабрика не отвечают требованиям из-за # 5.

static final отсутствует из-за # 4 (не может изменить финал). static пахнет из-за проблем с загрузчиком классов, но вам, вероятно, он понадобится внутри runOnce(). Я просто предпочел бы избежать этого в LHS выражения.

Одним из возможных решений может быть использование ehcache с настройкой по умолчанию, которая возвращает тот же объект. Поскольку я могу поместить вещи в кеш, это также позволит переопределить значение в любое время. Но, возможно, есть более компактное / простое решение, чем ehcache (для которого снова требуется файл конфигурации XML и т. Д.).

[РЕДАКТИРОВАТЬ] Мне интересно, почему так много людей отрицает это. Это правильный вопрос, и случай использования довольно распространен (по крайней мере, в моем коде). Так что, если вы не понимаете вопрос (или причину, стоящую за ним), или если у вас нет ответа, или вам все равно, зачем понижать голос? : /

[EDIT2] Если вы посмотрите на контекст приложения Spring, вы обнаружите, что более 99% всех компонентов имеют только одну реализацию. Вы могли бы иметь больше, но на практике вы просто не делаете. Поэтому вместо разделения интерфейса, реализации и конфигурации я смотрю на то, что имеет только реализацию (в самом простом случае), метод current () и одну или две строки умного кода для инициализации результата для current () один раз (когда он вызывается впервые), но в то же время позволяет переопределить результат (потокобезопасен, если возможно). Думайте об этом как о атомарном «if (o == null) o = new O (); return o», где вы можете переопределить o. Может быть, класс AtomicRunOnceReference является решением.

Прямо сейчас, я просто чувствую, что то, что мы все имеем и используем ежедневно, не является оптимальным, что есть сбивающее с толку решение, которое заставит нас всех бить нас по голове и говорить «все». Так же, как мы почувствовали, когда Spring появился несколько лет назад, и мы поняли, откуда возникли все наши проблемы синглтонов и как их решить.

Ответы [ 4 ]

6 голосов
/ 14 января 2009

Лучшая идиома для потокового кода инициализации (imho) - это ленивый внутренний класс. Классическая версия

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

    static {
      // create the SINGLETON
    }
  }

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

потому что он потокобезопасен, лениво загружается и (imho) элегантен.

Теперь вам нужны тестируемость и заменяемость. Трудно советовать, не зная, что именно, но самым очевидным решением было бы использование внедрения зависимостей, особенно если вы используете Spring и в любом случае имеете контекст приложения.

Таким образом, поведение вашего "синглтона" представлено интерфейсом, и вы просто внедряете один из них в соответствующие классы (или фабрику для его создания), а затем, конечно, можете заменить его на то, что вам нравится для тестирования цели.

3 голосов
/ 17 марта 2011

Вот решение, которое отвечает всем моим требованиям:

/** 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;
    }
}
1 голос
/ 14 января 2009

Вы можете использовать методы IoC, даже если вы не используете инфраструктуру IoC (Spring / Guice / ...). И это, по моему мнению, единственный чистый способ избежать Синглтона.

0 голосов
/ 14 января 2009

Я думаю, что одним из решений, которое вы могли бы использовать, является предоставление защищенного метода, который можно переопределить в тесте (решение, которое я использовал ранее для тестирования устаревшего кода).

Так что-то вроде:

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;
      }
   };
}

Я должен сказать, что я не слишком заинтересован в этом паттерне, поскольку он предоставляет защищенное поле там, где вы, возможно, не хотите его использовать, но он полезен для того, чтобы вывести вас из некоторых ситуаций, когда инъекция не подходит

...