Disposable паттерн (интерфейс IDisposable) предполагает возможность высвобождения некоторых ресурсов, занимаемых объектом, путём вызова метода Dispose, ещё до того момента, когда все ссылки на экземпляр будут утрачены и сборщик мусора утилизирует его (хотя для надёжности вызов Dispose часто дублируется в финализаторе).
Но существует также обратный Exposable паттерн, когда ссылка на объект становится доступной до момента его полной инициализации. То есть экземпляр уже присутствует в памяти, частично проинициализирован и другие объекты ссылаются на него, но, чтобы окончательно подготовить его к работе, нужно выполнить вызов метода Expose. Опять же данный вызов допустимо выполнять в конструкторе, что диаметрально вызову Dispose в финализаторе.
Само по себе наличие такой обратной симметрии выглядит красиво и естественно, но где это может пригодиться на практике постараемся раскрыть в этой статье.
Для справки, в C# существует директива using — синтаксический сахар для безопасного вызова метода Dispose.
эквивалентно
с той лишь разницей, что в первом случае переменная context становится read-only.
Unit of Work + Disposable + Exposable = Renewable Unit
Dispose-паттерну часто сопутствует паттерн Unit of Work, когда объекты предназначены для одноразового использовании, а время их жизни обычно короткое. То есть они создаются, тут же используются и затем сразу же освобождают занятые ресурсы, становясь непригодными для дальнейшего употребления.
Например, такой механизм часто применяется для доступа к сущностям базы данных через ORM-фреймворки.
Открывается соединение к БД, совершается нужный запрос, а затем оно сразу закрывается. Держать соединение постоянно открытым считается плохой практикой, поскольку зачастую ресурс соединений ограничен, а также соединения автоматически закрываются после определённого интервала бездействия.
Всё хорошо, но если у нас сервер с неравномерной нагрузкой, то в часы-пик на запросы пользователей будут создаваться огромные количества таких экземпляров объектов DbContext, что начнёт оказывать влияние на потребляемую сервером память и быстродействие, поскольку сборщик мусора станет вызываться чаще.
Здесь может помочь совместное использование паттернов Disposable и Exposable. Вместо того, чтобы постоянно создавать и удалять объекты достаточно создать один объект, а затем в нём же занимать и освобождать ресурсы.
Конечно, этот код не станет работать с существующими фреймворками, поскольку в них не предусмотрен метод Expose, но важно показать именно сам принцип — объекты можно использовать повторно, а необходимые ресурсы возобновлять динамически.
* Как заметили в комментария, возможно, это не самый удачный пример, поскольку прирост производительности достаточно спорный. Но чтобы лучше уловить суть рассматриваемого паттерна приведём следующие рассуждения.
В обычном понимании Disposable — деинициализация и полный отказ от объекта. Однако ссылка на него вполне может оставаться и после вызова Dispose. Зачастую обращение к большинству свойств и методов вызовет исключение, если программист это предусмотрел, но экземпляр обычно запросто можно использовать в качестве ключа, вызывать ToString, Equals и некоторые другие методы. Так почему бы не расширить понимание паттерна Disposable? Пусть Dispose приводит объект в дежурное состояние, когда он занимает меньше ресурсов, в спящий режим! Но тогда должен существовать и метод выводящий из этого состояния — Expose. Всё очень закономерно и логично. То есть мы получили некоторое обобщение паттерна Disposable, а сценарий с отказом от объекта — это лишь его частный случай.
Независимые инжекции путём экспанирования (Independent Injections via Exposable Pattern)
Важно! Для полного понимания нижесказанного очень рекомендуется загрузить исходные коды (резервная ссылка) библиотеки Aero Framework с примером текстового редактора Sparrow, а также желательно ознакомиться с серией предыдущих статей.
Расширения привязки и xaml-разметки на примере локализации
Инжекторы контекста xaml
Командно-ориентированная навигация в xaml-приложениях
Совершенствуем xaml: Bindable Converters, Switch Converter, Sets
Сахарные инжекции в C#
Context Model Pattern via Aero Framework
Классический способ инжектирование вью-моделей в конструктор с помощью unit-контейнеров выглядит так:
Но такой код вызовет исключение, поскольку невозможно проинициализировать ProductsViewModel пока не создана SettingsViewModel и наоборот.
Однако использование Exposable-паттерна в библиотеке Aero Framework позволяет элегантно решить проблему замкнутых зависимостей:
Вкупе с механизмом сохранения состояния (Smart State, о котором чуть ниже) это даёт возможность безопасно проинициализировать обе вью-модели, ссылающиеся друг на друга, то есть реализовать принцип независимых прямых инжекций.
Умное состояние (Smart State)
Теперь мы подошли к весьма необычному, но в то же время полезному механизму сохранения состояния. Aero Framework позволяет очень изящно и непревзойденно лаконично решать задачи подобного рода.
Запустите десктоп-версию редактора Sparrow, который является примером приложения к библиотеке. Перед вами обычное окно, которое можно перетащить или изменить в размерах (визуальное состояние). Также можно создать несколько вкладок либо открыть текстовые файлы, а затем отредактировать в них текст (логическое состояние).
После этого закройте редактор (нажмите крестик на окне) и запустите его снова. Программа запустится ровно в том же визуальном и логическом состоянии, в котором её закрыли, то есть размеры и положение окна будут прежними, останутся открытыми рабочие вкладки и даже текст в них будет в таким же, каким его оставили при закрытии! Между тем исходные коды вью-моделей на первый взгляд не содержат никакой вспомогательной логики для сохранения состояния, как так получилось?
При внимательном расмотрении вы, возможно, заметите, что вью-модели в примере приложения Sparrow отмечены атрибутом DataContract, а некоторые свойства атрибутом DataMember, что позволяет применять механизмы сериализации и десериализации для сохранения и восстановления логического состояния.
Всё, что нужно для этого выполнить, это проинициализировать необходимым образом фреймворк во время запуска приложения:
По умолчанию сериализация происходит в файлы, но легко можно создать свою имплементацию и сохранять сериализованные объекты, например, в базу данных. Для этого нужно унаследоваться от интерфейса Unity.IApplication (по умолчанию имплементируется AppStorage). Что касается интерфейса Unity.IApplication (AppAssistent), то он необходим для культурных настроек при сериализации и в большинстве случаев можно ограничиться его стандартной реализацией.
Для сохранения состояния любого объекта, поддерживающего сериализацию, достаточно вызвать аттачед-метод Snapshot, либо воспользоваться вызовом Store.Snapshot, если объект находится в общем контейнере.
Мы разобрались с сохранением логического состояния, но ведь зачастую возникает необходимость хранения и визуального, к примеру, размеров и положения окон, состояния контролов и других параметров. Фреймворк предлагает нестандартное, но невероятно удобное решение. Что если хранить такие параметры в контекстных объектах (вью-моделях), но не в виде отдельных свойств для сериализации, а неявно, в виде словаря, где ключом является имя «мнимого» свойства?
На основе данной концепции родилась идея smart-свойств. Значение smart-свойства должно быть доступно через индексатор по имени-ключу, как в словаре, а классический get или set являются опциональными и могут отсутствовать! Эта функциональность реализована в классе SmartObject, от которого наследуется ContextObject, расширяя её.
Достаточно всего лишь написать в десктоп-версии:
после чего размеры и положение окна будут автоматически сохраняться при выходе из приложения и в точности восстанавливаться при запуске! Согласитесь, это поразительная лаконичность для решения такого рода задачи. Во вью-модели или код-бехаин не пришлось писать ни одной дополнительной строчки кода.
* О небольших нюансах и ограничениях некоторых других xaml-платформ, а также способах из обхода следует смотреть оригинальную статью Context Model Pattern via Aero Framework.
Благодаря механизму полиморфизма валидация значений свойств с помощью имплементации интерфейса IDataErrorInfo, также использующего индексатор, очень изящно вписывается в концепцию смарт-состояния.
Итоги
Может показаться, что мы отклонились от основной темы, но это не так. Все, описанные в этой и предыдущих статьях, механизмы вместе с использованием паттерна Exposable позволяют создавать очень чистые и лаконичные вью-модели.
То есть запросто может получиться так, что во вью-модели объявлено несколько свойств и только один метод Expose, а весь остальной функционал описыватся лямбда-выражениями! А если планируется дальнейшее наследование, то следует просто отметить метод модификатором virtual .
Но существует также обратный Exposable паттерн, когда ссылка на объект становится доступной до момента его полной инициализации. То есть экземпляр уже присутствует в памяти, частично проинициализирован и другие объекты ссылаются на него, но, чтобы окончательно подготовить его к работе, нужно выполнить вызов метода Expose. Опять же данный вызов допустимо выполнять в конструкторе, что диаметрально вызову Dispose в финализаторе.
Само по себе наличие такой обратной симметрии выглядит красиво и естественно, но где это может пригодиться на практике постараемся раскрыть в этой статье.
Для справки, в C# существует директива using — синтаксический сахар для безопасного вызова метода Dispose.
using(var context = new Context())
{
// statements
}
эквивалентно
var context = new Context();
try
{
// statements
}
finally
{
if (context != null) context .Dispose();
}
с той лишь разницей, что в первом случае переменная context становится read-only.
Unit of Work + Disposable + Exposable = Renewable Unit
Dispose-паттерну часто сопутствует паттерн Unit of Work, когда объекты предназначены для одноразового использовании, а время их жизни обычно короткое. То есть они создаются, тут же используются и затем сразу же освобождают занятые ресурсы, становясь непригодными для дальнейшего употребления.
Например, такой механизм часто применяется для доступа к сущностям базы данных через ORM-фреймворки.
using(var context = new DbContext(ConnectionString))
{
persons =context.Persons.Where(p=>p.Age > minAge).ToList();
}
Открывается соединение к БД, совершается нужный запрос, а затем оно сразу закрывается. Держать соединение постоянно открытым считается плохой практикой, поскольку зачастую ресурс соединений ограничен, а также соединения автоматически закрываются после определённого интервала бездействия.
Всё хорошо, но если у нас сервер с неравномерной нагрузкой, то в часы-пик на запросы пользователей будут создаваться огромные количества таких экземпляров объектов DbContext, что начнёт оказывать влияние на потребляемую сервером память и быстродействие, поскольку сборщик мусора станет вызываться чаще.
Здесь может помочь совместное использование паттернов Disposable и Exposable. Вместо того, чтобы постоянно создавать и удалять объекты достаточно создать один объект, а затем в нём же занимать и освобождать ресурсы.
context.Expose();
persons = context.Persons.Where(p=>p.Age > minAge).ToList();
context.Dispose();
Конечно, этот код не станет работать с существующими фреймворками, поскольку в них не предусмотрен метод Expose, но важно показать именно сам принцип — объекты можно использовать повторно, а необходимые ресурсы возобновлять динамически.
* Как заметили в комментария, возможно, это не самый удачный пример, поскольку прирост производительности достаточно спорный. Но чтобы лучше уловить суть рассматриваемого паттерна приведём следующие рассуждения.
В обычном понимании Disposable — деинициализация и полный отказ от объекта. Однако ссылка на него вполне может оставаться и после вызова Dispose. Зачастую обращение к большинству свойств и методов вызовет исключение, если программист это предусмотрел, но экземпляр обычно запросто можно использовать в качестве ключа, вызывать ToString, Equals и некоторые другие методы. Так почему бы не расширить понимание паттерна Disposable? Пусть Dispose приводит объект в дежурное состояние, когда он занимает меньше ресурсов, в спящий режим! Но тогда должен существовать и метод выводящий из этого состояния — Expose. Всё очень закономерно и логично. То есть мы получили некоторое обобщение паттерна Disposable, а сценарий с отказом от объекта — это лишь его частный случай.
Независимые инжекции путём экспанирования (Independent Injections via Exposable Pattern)
Важно! Для полного понимания нижесказанного очень рекомендуется загрузить исходные коды (резервная ссылка) библиотеки Aero Framework с примером текстового редактора Sparrow, а также желательно ознакомиться с серией предыдущих статей.
Расширения привязки и xaml-разметки на примере локализации
Инжекторы контекста xaml
Командно-ориентированная навигация в xaml-приложениях
Совершенствуем xaml: Bindable Converters, Switch Converter, Sets
Сахарные инжекции в C#
Context Model Pattern via Aero Framework
Классический способ инжектирование вью-моделей в конструктор с помощью unit-контейнеров выглядит так:
public class ProductsViewModel : BaseViewModel
{
public virtual void ProductsViewModel(SettingsViewModel settingsViewModel)
{
// using of settingsViewModel
}
}
public class SettingsViewModel : BaseViewModel
{
public virtual void SettingsViewModel(ProductsViewModel productsViewModel)
{
// using of productsViewModel
}
}
Но такой код вызовет исключение, поскольку невозможно проинициализировать ProductsViewModel пока не создана SettingsViewModel и наоборот.
Однако использование Exposable-паттерна в библиотеке Aero Framework позволяет элегантно решить проблему замкнутых зависимостей:
public class ProductsViewModel : ContextObject, IExposable
{
public virtual void Expose()
{
var settingsViewModel = Store.Get<SettingsViewModel>();
this[Context.Get("AnyCommand")].Executed += (sender, args) =>
{
// safe using of settingsViewModel
}
}
}
public class SettingsViewModel : ContextObject, IExposable
{
public virtual void Expose()
{
var productsViewModel = Store.Get<ProductsViewModel>();
this[Context.Get("AnyCommand")].Executed += (sender, args) =>
{
// safe using of productsViewModel
}
}
}
Вкупе с механизмом сохранения состояния (Smart State, о котором чуть ниже) это даёт возможность безопасно проинициализировать обе вью-модели, ссылающиеся друг на друга, то есть реализовать принцип независимых прямых инжекций.
Умное состояние (Smart State)
Теперь мы подошли к весьма необычному, но в то же время полезному механизму сохранения состояния. Aero Framework позволяет очень изящно и непревзойденно лаконично решать задачи подобного рода.
Запустите десктоп-версию редактора Sparrow, который является примером приложения к библиотеке. Перед вами обычное окно, которое можно перетащить или изменить в размерах (визуальное состояние). Также можно создать несколько вкладок либо открыть текстовые файлы, а затем отредактировать в них текст (логическое состояние).
После этого закройте редактор (нажмите крестик на окне) и запустите его снова. Программа запустится ровно в том же визуальном и логическом состоянии, в котором её закрыли, то есть размеры и положение окна будут прежними, останутся открытыми рабочие вкладки и даже текст в них будет в таким же, каким его оставили при закрытии! Между тем исходные коды вью-моделей на первый взгляд не содержат никакой вспомогательной логики для сохранения состояния, как так получилось?
При внимательном расмотрении вы, возможно, заметите, что вью-модели в примере приложения Sparrow отмечены атрибутом DataContract, а некоторые свойства атрибутом DataMember, что позволяет применять механизмы сериализации и десериализации для сохранения и восстановления логического состояния.
Всё, что нужно для этого выполнить, это проинициализировать необходимым образом фреймворк во время запуска приложения:
Unity.AppStorage = new AppStorage();
Unity.App = new AppAssistent();
По умолчанию сериализация происходит в файлы, но легко можно создать свою имплементацию и сохранять сериализованные объекты, например, в базу данных. Для этого нужно унаследоваться от интерфейса Unity.IApplication (по умолчанию имплементируется AppStorage). Что касается интерфейса Unity.IApplication (AppAssistent), то он необходим для культурных настроек при сериализации и в большинстве случаев можно ограничиться его стандартной реализацией.
Для сохранения состояния любого объекта, поддерживающего сериализацию, достаточно вызвать аттачед-метод Snapshot, либо воспользоваться вызовом Store.Snapshot, если объект находится в общем контейнере.
Мы разобрались с сохранением логического состояния, но ведь зачастую возникает необходимость хранения и визуального, к примеру, размеров и положения окон, состояния контролов и других параметров. Фреймворк предлагает нестандартное, но невероятно удобное решение. Что если хранить такие параметры в контекстных объектах (вью-моделях), но не в виде отдельных свойств для сериализации, а неявно, в виде словаря, где ключом является имя «мнимого» свойства?
На основе данной концепции родилась идея smart-свойств. Значение smart-свойства должно быть доступно через индексатор по имени-ключу, как в словаре, а классический get или set являются опциональными и могут отсутствовать! Эта функциональность реализована в классе SmartObject, от которого наследуется ContextObject, расширяя её.
Достаточно всего лишь написать в десктоп-версии:
public class AppViewModel : SmartObject // ContextObject
{}
<Window
x:Class="Sparrow.Views.AppView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Sparrow.ViewModels"
DataContext="{Store Key=viewModels:AppViewModel}"
WindowStyle="{ViewModel DefaultValue=SingleBorderWindow}"
ResizeMode="{Binding '[ResizeMode, CanResizeWithGrip]', Mode=TwoWay}"
Height="{Binding '[Height, 600]', Mode=TwoWay}"
Width="{ViewModel DefaultValue=800}"
Left="{ViewModel DefaultValue=NaN}"
Top="{Binding '[Top, NaN]', Mode=TwoWay}"
Title="{ViewModel DefaultValue='Sparrow'}"
Icon="/Sparrow.png"
ShowActivated="True"
Name="This"/>
после чего размеры и положение окна будут автоматически сохраняться при выходе из приложения и в точности восстанавливаться при запуске! Согласитесь, это поразительная лаконичность для решения такого рода задачи. Во вью-модели или код-бехаин не пришлось писать ни одной дополнительной строчки кода.
* О небольших нюансах и ограничениях некоторых других xaml-платформ, а также способах из обхода следует смотреть оригинальную статью Context Model Pattern via Aero Framework.
Благодаря механизму полиморфизма валидация значений свойств с помощью имплементации интерфейса IDataErrorInfo, также использующего индексатор, очень изящно вписывается в концепцию смарт-состояния.
Итоги
Может показаться, что мы отклонились от основной темы, но это не так. Все, описанные в этой и предыдущих статьях, механизмы вместе с использованием паттерна Exposable позволяют создавать очень чистые и лаконичные вью-модели.
public class HelloViewModel : ContextObject, IExposable
{
public string Message
{
get { return Get(() => Message); }
set { Set(() => Message, value); }
}
public virtual void Expose()
{
this[() => Message].PropertyChanged += (sender, args) => Context.Make.RaiseCanExecuteChanged();
this[Context.Show].CanExecute += (sender, args) => args.CanExecute = !string.IsNullOrEmpty(Message);
this[Context.Show].Executed += async (sender, args) =>
{
await MessageService.ShowAsync(Message);
};
}
}
То есть запросто может получиться так, что во вью-модели объявлено несколько свойств и только один метод Expose, а весь остальной функционал описыватся лямбда-выражениями! А если планируется дальнейшее наследование, то следует просто отметить метод модификатором virtual .