среда, 1 декабря 2010 г.

“В жизни всегда есть место для подвига” © или как реализовать Singleton по-новому

Тема реализации шаблона Singleton так или иначе затрагивает жизнь любого разработчика – толи на собеседовании спросят (или самом спросить придется), толи в реальном проекте найдется применение (не приведи Господи). О том, как этого зверя вырастить, написано немало – и в MSDN, и в прочих интернетах (последняя статься считается канонической и ранее располагалась по другому адресу). В какой-то момент казалось (мне по крайней мере), что придумать что-нибудь новое в этой области невозможно. Ошибся.

Недавно на одном из проектов заказчик прислал кусок [псевдо]кода, в котором кроме всего прочего содержалась крайне любопытная реализация привычного шаблона (тот еще затейник – архитектура решения тоже заставляем “много думать”). В чистом виде (убирая конкретику проекта и применяя привычные для .NET схемы именования и форматирования) это выглядит так:





public interface ISingleton
{

}

class Singleton : ISingleton
{
public delegate ISingleton InstanceGetterDelegate();

private static Singleton _singleton;
private static InstanceGetterDelegate _instanceGetter;
static Singleton()
{
var type = System.Reflection.MethodBase.GetCurrentMethod().DeclaringType;
// assign the closure that executes on the first call of GetInstance() method
_instanceGetter = () =>
{
Singleton instance = null;
lock (type)
{
var rslt = Interlocked.CompareExchange<Singleton>(ref _singleton, null, null);
if (rslt == null) // indicates is first call
{
instance = new Singleton(/* init arguments go here*/);
Interlocked.CompareExchange<Singleton>(ref _singleton, null, instance);

// now replace with a closure that executes on all subsequent calls of GetInstance() method
InstanceGetterDelegate fn = () =>
{
return Interlocked.CompareExchange<Singleton>(ref _singleton, null, null);
};
Interlocked.Exchange<InstanceGetterDelegate>(ref _instanceGetter, fn);
}
else
{
instance = rslt; // was already set with singleton instance
}
}
return instance;
};
}
private Singleton() { } // disable default instance constructor
public static ISingleton GetInstance() { return _instanceGetter(); }
}


И вот тут, что называется, “накрывает”. Я бы назвал этот способ “JavaScript-style Singleton” (конечно же за использования замыканий). В целом (за исключение блокировки по Type (о чем я уже писал) и использования рефлексии в статическом конструкторе) реализация выглядит любопытной и уж во всяком случае расширяет горизонт.



HTH, AlexS

3 комментария:

  1. Вы правы, действительно "накрывает" :)

    Идея подмены делегата действительно свежая (по крайней мере для меня). Все остальное на мой взгляд полный булщит :)

    Мои комментарии:
    1) Все вызовы Interlocked.Exchange абсолютно бесполезны т.к. уже обернуты Lock'ом.
    2) InstanceGetterDelegate() и GetInstance() должны возвращать Singleton, а не ISingleton иначе прийдется кастовать...
    3) ISingleton в большинстве случаев вообще не нужен

    Итого мы имеем:

    class Singleton : ISingleton
    {
    public delegate Singleton InstanceGetterDelegate();

    private static Singleton _singleton;
    private static InstanceGetterDelegate _instanceGetter;
    private static readonly Object _syncObj = new Object();

    static Singleton()
    {
    // assign the closure that executes on the first call of GetInstance() method
    _instanceGetter = () =>
    {
    lock (_syncObj)
    {
    if (_singleton == null) // indicates is first call
    {
    _singleton = new Singleton(/* init arguments go here*/);
    // now replace with a closure that executes on all subsequent calls of GetInstance() method
    _instanceGetter = () =>
    {
    return _singleton;
    };
    }
    }
    return _singleton;
    };
    }
    private Singleton() { } // disable default instance constructor
    public static Singleton GetInstance() { return _instanceGetter(); }
    }

    p.s. С lock(type) не все однозначно.
    Вот тут (http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.71).aspx) например сказано следующее:

    "Typically, expression will either be this, if you want to protect an instance variable, or typeof(class), if you want to protect a static variable (or if the critical section occurs in a static method in the given class)."

    а тут (http://msdn.microsoft.com/en-us/library/c5kehkcz.aspx) вот что:

    "In general, avoid locking on a public type, or instances beyond your code's control. The common constructs lock (this), lock (typeof (MyType)), and lock ("myLock") violate this guideline:
    lock (this) is a problem if the instance can be accessed publicly.
    lock (typeof (MyType)) is a problem if MyType is publicly accessible.
    lock("myLock") is a problem because any other code in the process using the same string, will share the same lock."

    ОтветитьУдалить
  2. Сначала о главном: приведенный мною код - это очищенная от конкретики иллюстрация идеи, подсмотренной в одном из проектов. При этом автором кода я не являюсь. Отсюда - и Interlocked (в "прототипе" так и было), и блокировка, и интерфейс (в оригинале он был наполнен смыслом).

    Возвращаясь к вопросам:
    1) Да, Interlocked тут не особо помогает, но то сколько из-за lock, сколько потому, что присваивания и так атомарны (спасибо одному коллеге, что напомнил ;-) )
    2) Не понял мысли на счет преобразования типов. Не вижу проблемы в том, что делегаты возвращают ISingleton - наш класс Singleton реализует этот интерфейс, поэтому все будет в порядке.
    3) Интерфейс был в "оригинале", поэтому остался и тут (он добавляет веселья, но на сам пример большого влияние не оказывает).

    ОтветитьУдалить
  3. Саша, ты меня неправильно понял :)
    Я критиковал не тебя, а код и прекрасно понимаю, что ты его выложио "как есть".

    Возвращаясь к вопросам:
    1) Даже если присвоение были бы не атомарны Interlocked все равно не нужен т.к. в Lock мы все равно не пойдем больше одного раза.
    2) Тут все просто. Это нужно что бы не писать лишний каст: Singleton singleton = (Singleton)Singleton.GetInstance();

    ОтветитьУдалить