Blue Flower

Disposable паттерн (интерфейс IDisposable) предполагает возможность высвобождения некоторых ресурсов, занимаемых объектом, путём вызова метода Dispose, ещё до того момента, когда все ссылки на экземпляр будут утрачены и сборщик мусора утилизирует его (хотя для надёжности вызов Dispose часто дублируется в финализаторе).

Но существует также обратный 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 .