2 листопада 2010 р.

"Патерн Вівторка" #13: Одинак (Singleton)

Я знаю що Діма уже написав чудову статтю про Синглтон тут.

Але я всештки ще перекладу свою. Уявімо що нам потрібна глобальна логувальна система в програмі, також нам треба логувати наші повідомлення десь в якийсь файл, при чому нумеруючі повідомлення в не залежності із якого закутка приграми вилетіла помилка.


Як можна це зробити?



ОДИНАК

Клас зображений нижче демонструє просту реалізацію Дизай Патерну Одинак (ця назва чесно кажучи звучить дещо дико. Думаю що може буде краще його називати Синглтон. Як думаєте?)

public class LoggerSingleton {


    private LoggerSingleton(){}
   

    private int _logCount = 0;

    private static LoggerSingleton _loggerSingletonInstance = new LoggerSingleton();


    public static LoggerSingleton GetInstance(){

    return _loggerSingletonInstance;

    }



    public void Log(String message){

    Console.WriteLine(_logCount + ": " + message);

    _logCount++;

    }

}

Отже ми збираємося почати нашу роботу із методу doHardWork і ми хочемо залогувати факт того що вона почалася, що відбувалося і факт того, що вона закінчилася.

Наша "важка робота" може виглядати так:


public static void doHardWork() {

    LoggerSingleton logger = LoggerSingleton.GetInstance();

    HardProcessor processor = new HardProcessor(1);



    logger.Log("Hard work started...");



    processor.processTo(5);



    logger.Log("Hard work finished...");

}


Як бачимо у нас є деякий клас HardProcessor. Що він наспраді робить нас не дуже має хвилювати, але ми б хотіли, щоб було залоговоно коли він створився і коли він виконував якісь підрахунки.


public class HardProcessor {



    private int _start;



    public HardProcessor(int start){

    _start = start;

    LoggerSingleton.GetInstance().Log("Processor just created.");

    }

  

    public int processTo(int end){

    int sum = 0;

    for(int i = _start; i <= end; ++i){

       sum += i;

    }

    LoggerSingleton.GetInstance().Log("Processor just calculated some value: " + sum);

    return sum;

    }  

}


А ось вивід програми:

0: Processor just created.
1: Hard work started...
2: Processor just calculated some value: 15
3: Hard work finished...


Я написав цей приклад коли був у поїзді і показав його свому другу, який також є програмістом. Він мені сказав що я написав Моностейт. - Я? Де? Ми поглянули на код і як виявилося що тільки одна змінна в Синглтоні, яку я використовував, _logCount, була статичною в початковому варінті, який я написав. (Зараз вона змінена на змінню інстансу).


То ж в чому різниця між Синглтоном і Моностейтом?


Синглтон  можна розглядати як спосіб забезпечення одного інстансу класу для нашої аплікації. Моностейт (Monostate) взагалі кажучи робить те ж що і GOF Синглтон. Всі змінні є статичними, таким чином теоретично ви можете мати багато інстансів Моностейту, але ж статичні дані одні і ті ж для одного і того ж типу.

Таким чином це допомагає також вирішити проблеми багатопоточності.


Давайте глянемо на приклад ще раз. Швидше за все, що логгер не є таким тривіальним, можливо ви захочете добавити функціональні зберігання останнього номера логу і при наступному зчитуванні починати нумерацію саме із того номера.

Але й ваша програма не така вже й тривіальна і логування викликається не тільки із багатьох місць у коді а й із багатьох потоків.

Оскільки конструктор вашого класу працює із IO, то виктачається час на створення екземпляру класу. Таким чином GetInstance() може привезти до ситуації коли ви створете декілька екземплярів класу. Напевно не так вже й добре для вашої аплікації.

На щастя є багато способів вирішити цю проблему. Найчастіше використовується Double-Checked Locking.

public class ThreadSafeLoggerSingleton {



    private ThreadSafeLoggerSingleton() {

      //reads data from some file and gets latest number of message

       //_logCount = got value here...

    }


    private int _logCount = 0;


    private static ThreadSafeLoggerSingleton _loggerInstance;



    public static ThreadSafeLoggerSingleton GetInstance(){

    if (_loggerInstance == null) {

          synchronized (ThreadSafeLoggerSingleton.class) {

            if (_loggerInstance == null)

                _loggerInstance = new ThreadSafeLoggerSingleton();

          }

        }

        return _loggerInstance;

    }



    public void Log(String message) {

        Console.WriteLine(_logCount + ": " + message);

        _logCount++;

    }

}


Є багато способів вирішення проблеми і не всі реалізації DCL працють. Прочитайте ось цю дуже хорошу статтю: http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html



Як можна побачити в методі Log я використовую змінну класу і роблю якісь операції які є майже атомарними. Проте якщо б операції були б складнішими то синхронізація знадобилася б і там.


(Я сьогодні трішки в ауті із часом, тому код майже С#-повський :) )


Моя табличка Патернів

Developer's RoadMap To Success

8 коментарів:

  1. http://geekswithblogs.net/BlackRabbitCoder/archive/2010/05/19/c-system.lazylttgt-and-the-singleton-design-pattern.aspx - цікаве доповнення до статті Андрія, приєднуюсь до вітань

    ВідповістиВидалити
  2. Дуже сумнівний шаблон! Особливо з точки зору TDD (DI і т.і.) і особливо в классичній реалізації (як у прикладі)

    ВідповістиВидалити
  3. десь читав, що робити lock (typeof(SomeClass)) трохи зле і краще робити як у статті, на яку посилається Andy:
    var mutex = new object();
    lock(mutex)

    Правда не можу згадати чому )) І лінь шукати, може хто знає?

    А варіант з Lazy класний, просто не всім підійде, бо не всі можуть використовувати .NET 4.0

    ВідповістиВидалити
  4. >> І лінь шукати, може хто знає?
    Дія такого локу поширюється на весь AppDomain у який завантажено тип. Ситуація погіршується для domain-neutral типів таких як String.

    ВідповістиВидалити
  5. 2 Oleh: дякую! ) ти спас мене від гугл-фу ))

    ВідповістиВидалити