Pull to refresh

Exposable паттерн. Независимые инжекции путём экспанирования

Reading time7 min
Views15K
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 .
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments74

Articles