Название шаблона
Отложенная (ленивая) инициализация (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);
}
}
В данном случае, каждое значение будет вычислено перед началом очередного прохода цикла.