Andrey on .NET | Порождающие шаблоны: Отложенная инициализация

Порождающие шаблоны: Отложенная инициализация

Название шаблона

Отложенная (ленивая) инициализация (Lazy initialization).

Тип

Порождающий шаблон проектирования (Creational).

Описание

Шаблон "Отложенная инициализация" позволяет отложить действия, связанные с созданием объекта, до момента, когда непосредственно потребуется результат этих действий.

Данный шаблон используется если

  • создание объекта связано с большими затратами ресурсов;
  • есть вероятность, что объект или его часть не будут использованы.

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

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

Отложенная инициализация часто используется с такими шаблонами как "Одиночка", "Фабричный метод", "Абстрактная фабрика".

Реализация шаблона в общем виде

  • определяем, инициализация каких ресурсов объекта занимает большую часть времени;
  • для них создаем отдельные закрытые методы инициализации;
  • вызываем эти методы перед первым использованием ресурса.

Примеры реализации

1. Отложенная инициализация используемого объекта

Данный метод подходит в случаях, если нельзя изменить исходный код класса для реализации отложенной инициализации. Или если нет возможности разделить инициализацию ресурсов класса и можно отложить только выполнение конструктора полностью.

В .NET 4 для этого есть готовое решение – generic-класс Lazy<T>. Он создает экземпляр класса указанного типа при первом обращении к нему. Использование его очень простое:

public class LogFile
{
    public LogFile()
    {
        Console.WriteLine("LogFile constructor");
    }

    public void AddToLog(string msg) { }
}

public class LazyLogDemo
{
    private readonly Lazy<LogFile> _lazyLog = new Lazy<LogFile>();

    public LazyLogDemo()
    {
        string msg = string.Empty;
        bool isError = false;
        
        // Do something

        if (isError) { this._lazyLog.Value.AddToLog(msg); }
    }

    public void Execute()
    {
        string msg = string.Empty;
        bool isError = true;

        // Do something

        if (isError) { this._lazyLog.Value.AddToLog(msg); }
    }
}

В данном примере видно, что экземпляр класса LogFile не обязательно будет использоваться в ходе выполнения программы. Поэтому его инициализация отложена до его первого вызова любого его метода или свойства. Доступ к самому экземпляру осуществляется через свойство Value класса Lazy<T>. А определить, был ли он инициализирован, поможет свойство IsValueCreated.

Кроме того, возможны ситуации, когда необходимо использовать конструктор с параметрами. В других случаях процедура создания объекта не сводится только к использованию ключевого слова new. Все это предусмотрено в конструкторе Lazy<T>. Нужно всего лишь передать ему метод, который будет отвечать за создание объекта. Например:

var log = new Lazy<LogFile>(() => {
            var obj = new LogFile() { MsgLevel = 5 };
            return obj;
        }
);

В случае работы с объектом из различных потоков необходимо указать режим потокобезопасности. Для этого используются значения перечисления LazyThreadSafetyMode. Возможны следующие варианты:

  • None – потокобезопасность инициализации хранимого объекта обеспечивается им самим;
  • PublicationOnly – при попытке одновременной инициализации из нескольких потоков, каждый из них запустит конструктор, но будет сохранен и использован только первый успешный результат;
  • ExecutionAndPublication – гарантирует что конструктор будет запущен только одним потоком.

Еще один пример отложенной инициализации был рассмотрен при описании шаблона "Одиночка".

2. Разделение инициализации ресурсов объекта

Рассмотрим следующую ситуацию: есть ряд серверов, принадлежащих удаленным офисам и доступных через интернет. Есть готовый класс для получения данных – DataSource. Его инициализация занимает определенное время, т.к. запрашиваются некий набор исходных данных. Задача – считать из конфигурации список офисов (метод GetOfficesList()) и предоставлять данные на указанную дату. При этом не обязательны, но возможны одновременные запросы к различным серверам. Кроме того, в ходе работы программы могут быть опрошены не все сервера.

Прямое решение – создать все объекты для связи сразу в конструкторе. Например, вот так:

public class OfficeDataManager
{
    private Dictionary<int, DataSource> _offices = new Dictionary<int, DataSource>();

    public OfficeDataManager()
    {
        OfficeInfo[] officeInfo = this.GetOfficesList();

        foreach (OfficeInfo info in officeInfo) {
            DataSource dataSource = new DataSource(info);
            this._offices.Add(info.Id, dataSource);
        }
    }
}

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

Изменим тактику инициализации. Используем класс Lazy<T> для того, чтобы инициализация DataSource происходила только перед его реальным использованием.

public class LazyOfficeDataManager
{
    private Dictionary<int, Lazy<DataSource>> _offices
        = new Dictionary<int, Lazy<DataSource>>();

    public LazyOfficeDataManager()
    {
        OfficeInfo[] officeInfo = this.GetOfficesList();

        foreach (OfficeInfo info in officeInfo) {
            OfficeInfo infoForClosure = info;

            Lazy<DataSource> dataSource = new Lazy<DataSource>(() => {
                var obj = new DataSource(infoForClosure);
                return obj;
            }, LazyThreadSafetyMode.ExecutionAndPublication);

            this._offices.Add(info.Id, dataSource);
        }
    }
}

Новый вариант лишен указанных выше недостатков:

  • частично снимается вопрос с задержкой выполнения при создании объектов, т.к. этот процесс "размазан" по времени;
  • запрос исходных данных теперь будет происходить только при необходимости получения связи с сервером офиса;
  • кроме того, благодаря режиму ExecutionAndPublication, создание объекта гарантировано потокобезопасное и будет происходить только один раз.

Небольшое пояснение для тех, кого смутила строчка OfficeInfo infoForClosure = info; Это не ошибка и не лишний код. Такой подход используется для корректного захвата переменной цикла внутри анонимной функции инициализации объекта. Не будем останавливаться на этом подробно, т.к. это отдельная тема. Её детали хорошо раскрыты в статье "Замыкания в языке C#", опубликованной на сайте RSDN.

3. Частный случай: использование ключевого слова yield

И в завершении рассмотрим вариант с ключевым словом yield.

Используя yield return и yield break можно создавать методы, которые возвращают перечисления. При этом компилятор самостоятельно создает класс, который реализует IEnumerable и использует исходный метод для вычисления значений самого перечисления. C подробностями реализации можно ознакомиться в статье "Использование ключевого слова yield".

Как все это относится к отложенной инициализации? Дело в том, что очередное значение перечисления вычисляется только в тот момент, когда оно необходимо. Например, перечисление степеней от 1 до exponent для заданного числа number:

public class PowerList
{
    public IEnumerable Power(int number, int exponent)
    {
        int valueCounter = 0;
        int currentResult = 1;
        while (valueCounter++ < exponent) {
            currentResult = currentResult * number;
            yield return currentResult;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        PowerList demoObj = new PowerList();

        // Display powers of 2 up to the exponent 10
        foreach (int i in demoObj.Power(2, 10)) {
            Console.Write("{0} ", i);
        }

        Console.ReadKey(true);
    }
}

В данном случае, каждое значение будет вычислено перед началом очередного прохода цикла.

Комментарии (4) -

Дмитрий 10.08.2011 20:16:45

Отличная статья. Не знал про Lazy<T>, а про yield return позабыл, увлекшись более сложными вещами. Спасибо!

Пожалуйста.
Заодно подправил ссылку на статью про yield.

Дмитрий 24.01.2015 4:58:45

>> this._offices.Add(info.Id, dataSource);

Почему не ....dataSurce.Value ?

Дмитрий 24.01.2015 5:00:55

а, dataSource.Value  будет при обращении к конкретному TValue и тут заработает отложенная инициализация.

Спасибо! Отличная статья

Добавить комментарий