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 .

Комментарии (74)


  1. lair
    26.04.2015 19:24
    +4

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

    И что вы будете делать, когда к одному объекту параллельно обратятся два разных потока, и один из них скажет Expose, а второй — Dispose?

    Всё хорошо, но если у нас сервер с неравномерной нагрузкой, то в часы-пик на запросы пользователей будут создаваться огромные количества таких экземпляров объектов DbContext, что начнёт оказывать влияние на потребляемую сервером память и быстродействие, поскольку сборщик мусора станет вызываться чаще.
    У вас есть реальные измерения производительности в таких сценариях?


    1. Makeman Автор
      26.04.2015 22:39
      -1

      Самое очевидное, что можно сделать для синхронизации:

      lock(context)
      {
          context.Expose();
          persons = context.Persons.Where(p=>p.Age > minAge).ToList();
          context.Dispose();
      }
      

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

      Реальных измерений производительности в описанных сценариях у меня нет, тут могут оказывать влияние многие факторы, например, ресурсоёмкость объекта DbContext, наличие свободной оперативной памяти, количество и временное распределение запросов. Суть в том, чтобы показать, что существует такой паттерн Exposable обратный Disposable и их возможно применять как по раздельности, так и вместе.


      1. lair
        27.04.2015 00:32
        +2

        Самое очевидное, что можно сделать для синхронизации:

        И вы после этого говорите о производительности? Когда вы поставили все запросы к БД в одну очередь, причем зависящую от времени выполнения каждого из запросов, включая самые длинные?

        Реальных измерений производительности в описанных сценариях у меня нет

        Тогда на основании чего вы делаете выводы о том, что предлагаемое вами решение дает выигрыш в производительности?

        Суть в том, чтобы показать, что существует такой паттерн Exposable

        А кто-нибудь, кроме вас, его признает?


        1. Makeman Автор
          27.04.2015 10:02
          -1

          Хорошо, всё те же условия — сервер с неравномерной загрузкой. Запросы могут идти как по одному, так и большими «пачками».

          Решение:
          Когда приходит запрос, проверяется по таймеру состояние объекта context и, если он не готов к работе, вызывается метод Expose, после чего включается таймер, например, на секунд 10. Если запросов больше не было, то спустя этот промежуток вызывается Dispose, иначе таймер продлевается. В результате соединение слишком долго не держится и не нужно постоянно создавать и удалять context. По-моему, по такой схеме вполне можно получить выигрыш в производительности. Не проверял это на практике, но исхожу из логических рассуждений.

          А кто-нибудь, кроме вас, его признает?

          Моя задача показать идею. Возможно, кто-то уже доходил до неё, но в других источниках её описания мне не встречалось.


          1. lair
            27.04.2015 10:53
            +1

            Когда приходит запрос, проверяется по таймеру состояние объекта context и, если он не готов к работе, вызывается метод Expose, после чего включается таймер, например, на секунд 10. Если запросов больше не было, то спустя этот промежуток вызывается Dispose, иначе таймер продлевается.

            И вас вот вообще не смущает, что DbContext — это UOW, и его нельзя разделять между разными клиентами?

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

            На основании чего вы думаете, что создание и уничтожение DbContext — такая дорогая операция?

            Моя задача показать идею. Возможно, кто-то уже доходил до неё, но в других источниках её описания мне не встречалось.

            Это означает, что не «существует паттерн Exposable», а «я нашел что-то похожее на паттерн». Так вот, то, что вы пытаетесь выше показать для DbContext, называется object pooling, и реализуется не так. А то, что вы показываете во вью-моделях — это обычный инициализационный метод. И это два разных паттерна для решения разных задач.


            1. Makeman Автор
              27.04.2015 14:19
              -1

              И вас вот вообще не смущает, что DbContext — это UOW, и его нельзя разделять между разными клиентами?

              В статье упоминается про это, но задача стоит в том, чтобы показать альтернативу UOW.

              На основании чего вы думаете, что создание и уничтожение DbContext — такая дорогая операция?

              Это не дорогая операция в большинстве реальных случаев. Возможно, выбран не самый удачный пример, но он хорошо иллюстрирует концепцию совместного применения Expose и Dispose методов.

              Это означает, что не «существует паттерн Exposable», а «я нашел что-то похожее на паттерн». Так вот, то, что вы пытаетесь выше показать для DbContext, называется object pooling, и реализуется не так. А то, что вы показываете во вью-моделях — это обычный инициализационный метод. И это два разных паттерна для решения разных задач.

              Даже если классический object pooling выглядит по-другому, то это не означает, что альтернативных реализаций быть не может. Если Expose — обычный инициализационный метод, то Dispose — деинициализационный — всё просто :)

              Но стоит учитывать те преимущества, которые даёт использование этого инициализационного метода перед классическим вызовом конструктора.

              Хочу лишь спросить, не замечаете ли вы своего рода гармонии и красоты в существовании Expose и Dispose, конструктора и финализатора? Это, конечно, странный вопрос, но лично меня в программировании цепляет такая стройность…


              1. lair
                27.04.2015 14:22
                +1

                В статье упоминается про это, но задача стоит в том, чтобы показать альтернативу UOW.

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

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

                Пока что, как вы видите, он вызывает больше вопросов, чем что-то иллюстрирует.

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

                Может — если они преследуют другую цель. Иначе вы нарушаете сам смысл понятия «паттерн».

                Если Expose — обычный инициализационный метод, то Dispose — деинициализационный — всё просто :)

                В том-то и дело, что Dispose — это не деинициализация, это полный отказ от объекта.

                Хочу лишь спросить, не замечаете ли вы своего рода гармонии и красоты в существовании Expose и Dispose, конструктора и финализатора?

                Нет, не замечаю. Я, наоборот, замечаю крупное семантическое расхождение.


                1. Makeman Автор
                  27.04.2015 15:05

                  В том-то и дело, что Dispose — это не деинициализация, это полный отказ от объекта.

                  В обычном понимании отказ, но если вдуматься, то не совсем так.Cсылка на объект вполне может оставаться и после вызова Dispose. Зачастую обращение к большинству свойств и методов вызовет исключение, если программист это предусмотрел, но экземпляр обычно запросто можно использовать в качестве ключа, вызывать ToString, Equals… Так почему бы не расширить понимание паттерна Disposable? Пусть метод приводит объект в дежурное состояние, когда он занимает меньше ресурсов, в спящий режим! Но тогда должен существовать и метод выводящий из этого состояния — Expose. Всё очень закономерно и логично. То есть мы получили некоторое обобщение паттерна Disposable, а ваш сценарий с отказом от объекта — это лишь его частный случай…


                  1. lair
                    27.04.2015 15:48
                    +1

                    Так почему бы не расширить понимание паттерна Disposable?

                    Потому что его семантика уже определена.

                    Пусть метод приводит объект в дежурное состояние, когда он занимает меньше ресурсов, в спящий режим!

                    Зачем? Чем вас не устраивает object pool, в котором все уже реализовано?


                    1. Makeman Автор
                      27.04.2015 16:02

                      Мой ответ, наверняка, вас не устроит — в этом есть красота :)
                      По крайней мере, вижу её там очень чётко.

                      Чем вас не устраивает object pool, в котором все уже реализовано?

                      Для своего круга задач вполне устраивает. Например пул потоков очень даже хорош.

                      Потому что его семантика уже определена.

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


                      1. lair
                        27.04.2015 16:06
                        +1

                        Для своего круга задач вполне устраивает.

                        Для какого круга задач он хуже, чем предложенное вами в начале статьи решение с парным Expose/Dispose?

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

                        Если вы обнаружили что-то новое, используйте это новое, а не трогайте уже существующее. Вы правда не понимаете смысла понятия «паттерн»/«шаблон проектирования»?


                        1. Makeman Автор
                          27.04.2015 16:35

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

                          Если вы обнаружили что-то новое, используйте это новое, а не трогайте уже существующее. Вы правда не понимаете смысла понятия «паттерн»/«шаблон проектирования»?

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


                          1. lair
                            27.04.2015 16:44
                            +1

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

                            Приведите пример такого объекта. Начать надо с вопрос «какой у этого объекта жизненный цикл».


                            1. Makeman Автор
                              27.04.2015 17:08

                              Пусть это будет провайдер данных для пользователя. Внутри он хранит информацию о сессии пользователя и кэш-данных. В Exposed состоянии (активная работа пользователя) держит соединение к БД, а в Disposed (простой или малая активность) ожидает запросов или использует только кэш.

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


                              1. lair
                                27.04.2015 17:28
                                +1

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


                                1. Makeman Автор
                                  27.04.2015 17:48

                                  Или тривиально решается при помощи Expose/Dispose с тем же разделением ответственности, но только провайдер не создаётся заново каждый раз, а выходит из режима ожидания. Просто альтернативные способы со своими плюсами и минусами. И каждый имеет право на жизнь.


                                  1. lair
                                    27.04.2015 18:18
                                    +2

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


                                    1. Makeman Автор
                                      27.04.2015 19:19

                                      Почему нет разделения ответственности? Всё так же, как и у вас, единственное отличие в том, что вместо new Provider(ConnectionString) будет _provider.Expose(). Если вам привычнее классический вариант, то используйте его, или назовите методы по-другому, суть паттерна останется прежней.

                                      Мне нравится связка Expose/Dispose, непривычно, но ничего сверхсложного в ней тоже нет.


                                      1. lair
                                        27.04.2015 20:48

                                        Давайте на примерах. Вот то, что предлагаете вы (если я не прав — поправьте):

                                        class UserDataProvider
                                        {
                                          public Guid UserId {get; set;}
                                          public IDictionary<string,object> SessionData {get;private set;}
                                        
                                          private SqlConnection _conn;
                                          public void Expose()
                                          {
                                            _conn = new SqlConnection(..);
                                            _conn.Open();
                                          }
                                        
                                          public void Dispose()
                                          {
                                             _conn.Dispose();
                                             _conn = null;
                                          }
                                        
                                          public X GetSomeData()
                                          {
                                             var cmd = _conn.CreateCommand();
                                             //...
                                          }
                                        }
                                        


                                        Ну и сразу возникают вопросы:

                                        • Что гарантирует вызов Dispose?
                                        • Что будет, если вызвать GetSomeData до вызова Expose?
                                        • Что будет, если внутри Expose, после первой строчки, будет эксепшн?


                                        А теперь мой вариант:

                                        interface IUserDataProvider: IDisposable
                                        {
                                          X GetSomeData()
                                        }
                                        
                                        class UserData
                                        {
                                          public Guid UserId {get; set;}
                                          public IDictionary<string,object> SessionData {get;private set;}
                                        
                                          public IUserDataProvider GetProvider()
                                          {
                                            return new UserDataProvider();
                                          }
                                        
                                          class UserDataProvider: IUserDataProvider 
                                          {
                                            private readonly SqlConnection _conn;
                                            private readonly UserData _userData;
                                        
                                            public UserDataProvider(UserData userData)
                                            {
                                              _userData = userData;
                                              _conn = new SqlConnection(..);
                                              _conn.Open();
                                            }
                                        
                                            public void Dispose()
                                            {
                                              _conn.Dispose();
                                            }
                                        
                                            public X GetSomeData()
                                            {
                                               var cmd = _conn.CreateCommand();
                                               //...
                                            }
                                          }
                                        }
                                        


                                        Ну и сразу ответы на вопросы выше:

                                        • Использование провайдера можно обернуть в using, он гарантирует вызов Dispose
                                        • Вызов GetSomeData до инициализации объекта невозможен
                                        • Если внутри конструктора будет эксепшн, объект сразу попадет в очередь GC, где и будет радостно собран вместе со всеми его зависимостями (включая вызов финализатора, если тотопределен)


                                        1. Makeman Автор
                                          28.04.2015 00:40
                                          -1

                                          Если внутри конструктора будет эксепшн, объект сразу попадет в очередь GC, где и будет радостно собран вместе со всеми его зависимостями (включая вызов финализатора, если тотопределен)

                                          Говорят, что эксепшены в конструкторах — это не очень хорошая практика. Но на мой взгляд это не критично, поэтому пускай будет так.

                                          Вызов GetSomeData до инициализации объекта невозможен

                                          Это столь же очевидно, как невозможность вызвова этого метода сразу после исполнения Dispose.

                                          Меня бы устроила примерно следующая реализация.

                                              internal class UserDataProvider : IExposable, IDisposable
                                              {
                                                  public Guid UserId { get; set; }
                                                  public IDictionary<string, object> SessionData { get; private set; }
                                          
                                                  private SqlConnection _conn;
                                          
                                                  public bool HasConnection { get { return _conn != null; } }
                                          
                                                  public UserDataProvider()
                                                  {
                                                      Expose();
                                                  }
                                          
                                                  ~UserDataProvider()
                                                  {
                                                      Dispose();
                                                  }
                                          
                                                  public void Expose()
                                                  {
                                                      if (HasConnection) return;
                                                      _conn = new SqlConnection();
                                                      _conn.Open();
                                                  }
                                          
                                                  public void Dispose()
                                                  {
                                                      if (!HasConnection) return;
                                                      _conn.Dispose();
                                                      _conn = null;
                                                  }
                                          
                                                  public X GetSomeData()
                                                  {
                                                      if (!HasConnection) Expose();
                                          
                                                      try
                                                      {
                                                          var cmd = _conn.CreateCommand();
                                                          //...
                                                      }
                                                      catch (TimeoutException exception)
                                                      {
                                                          _conn = null;
                                                          return GetSomeData();
                                                      }
                                                  }
                                              }
                                          


                                          Всё гарантировано вызывается, покрывается ваш случай и даже более. Плюс объект становится возобновляемым. Какие минусы, на ваш взгляд, у такой реализации и чем она хуже классической?


                                          1. lair
                                            28.04.2015 00:51

                                            Говорят, что эксепшены в конструкторах — это не очень хорошая практика.

                                            Врут. До тех пор, пока все ресурсы освобождаются в нужном объеме, никаких проблем это не вызывает.

                                            Это столь же очевидно, как невозможность вызвова этого метода сразу после исполнения Dispose.

                                            Это очевидно в моей реализации, но не в вашей.

                                            Какие минусы, на ваш взгляд, у такой реализации и чем она хуже классической?

                                            Собственно, один минус никуда не делся — что случится, если внутри Expose, на вызове _conn.Open, произойдет ошибка?

                                            Да и гарантии вызова Dispose не появилось — вы не можете обернуть вызов пары Expose/Dispose в using.

                                            Зато теперь все стало еще смешнее. Теперь при вызове GetSomeData незаметно для пользователя откроется соединение. А когда оно закроется? Никто не знает.

                                            Но ладно бы у вас было неявное состояние, это еще можно понять, но какой тогда смысл в методе Expose? Можно просто вызывать GetSomeData, поведение будет ровно таким же.

                                            PS Не надо вызывать Dispose в финализаторе, если вы не владеете неуправляемыми ресурсами.
                                            PPS Не надо при таймауте просто выкидвать коннекшн, вы его не освободили еще, и теперь он повиснет в пуле на некоторое неизвестное время.


                                            1. Makeman Автор
                                              28.04.2015 01:14

                                              Зато теперь все стало еще смешнее. Теперь при вызове GetSomeData незаметно для пользователя откроется соединение. А когда оно закроется? Никто не знает.

                                              Если вас эта строчка очень смущает, то можете её убрать. Гарантировано Dispose вызовется, когда ссылок на объект не останется.

                                              Но ладно бы у вас было неявное состояние, это еще можно понять, но какой тогда смысл в методе Expose? Можно просто вызывать GetSomeData, поведение будет ровно таким же.

                                              Есть ещё свойство HasConnection, поэтому при желании можно контролировать наличие соединения методами Expose/Dispose, или можно не контролировать если лень или это не критично.

                                              Собственно, один минус никуда не делся — что случится, если внутри Expose, на вызове _conn.Open, произойдет ошибка?

                                              Вызов можно обернуть в try-catch блок и произвести нужные действия в зависимости от типа исключения. В вашем же случае нужно оборачивать в try-catch весь блок using, что уже смотрится не очень, поскольку последний тоже разворачивается в try-finally.

                                              PPS Не надо при таймауте просто выкидвать коннекшн, вы его не освободили еще, и теперь он повиснет в пуле на некоторое неизвестное время.

                                              Здесь имеется в виду превышение интервала бездействия по которому соединение закрылось автоматически.

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


                                              1. lair
                                                28.04.2015 10:55
                                                +1

                                                Если вас эта строчка очень смущает, то можете её убрать.

                                                … и мы вернемся к ситуации «что будет, если вызвать GetSomeData до вызова Expose».

                                                Гарантировано Dispose вызовется, когда ссылок на объект не останется.

                                                А когда это произойдет для объекта, лежащего в сессии? Через полчаса после того, как пользователь покинет сайт? А сколько на сайте одновременных пользователей?

                                                Есть ещё свойство HasConnection, поэтому при желании можно контролировать наличие соединения методами Expose/Dispose, или можно не контролировать если лень или это не критично.

                                                А смысл? Поведение между Expose/GetSomeData и просто GetSomeData ничем не отличается. Второе даже лучше, потому что сеодинение захватывается попозже.

                                                В вашем же случае нужно оборачивать в try-catch весь блок using

                                                Вы не понимаете, похоже. В моем случае не надо ничего оборачивать в try-catch (параноики могут обернуть вызов _conn.Open в конструкторе, но это не обязательно), потому что объект хоть и будет создан, но на него не будет ни одной ссылки, поэтому он будет сразу собран GC. А в вашем случае некорректно инициализированное соединение будет висеть внутри вашего объекта, и никакого способа избавиться от него нет (если, конечно, вы не реализуете try-catch внутри Expose).

                                                Здесь имеется в виду превышение интервала бездействия по которому соединение закрылось автоматически.

                                                А ничего, что TimeoutException бывает не только в таких случаях, и не всегда гарантировано, что соединение закрыто?

                                                На мой взгляд плюс реализации в том, что она неявно, но безопасно выполняет всю работу по установке соединения

                                                К сожалению, только неявно. Никакой безопасности в вашей реализации нет, более того, вы с каждой версией порождаете новые потенциальные утечки.


                                                1. Makeman Автор
                                                  28.04.2015 11:25

                                                  … и мы вернемся к ситуации «что будет, если вызвать GetSomeData до вызова Expose».

                                                  Это точно такая же ситуация, что и с вызовом метода после Dispose, и ничего в ней нет особенного. Но мне больше нравится оригинальный вариант, где даже после Dispose объект сохраняет способность к автоматическому восстановлению соединения.

                                                  А когда это произойдет для объекта, лежащего в сессии? Через полчаса после того, как пользователь покинет сайт? А сколько на сайте одновременных пользователей?

                                                  Нет ничего нерешаемого в данной ситуации, если это достаточно критично для приложения.

                                                  А смысл? Поведение между Expose/GetSomeData и просто GetSomeData ничем не отличается. Второе даже лучше, потому что сеодинение захватывается попозже.

                                                  С помощью вызовов Expose/Dispose можно более точно контролировать время жизни соединения, если это нужно. Более того так очень легко проверить доступность сервера базы данных (протестировать соединение).

                                                  Вы не понимаете, похоже. В моем случае не надо ничего оборачивать в try-catch (параноики могут обернуть вызов _conn.Open в конструкторе, но это не обязательно), потому что объект хоть и будет создан, но на него не будет ни одной ссылки, поэтому он будет сразу собран GC. А в вашем случае некорректно инициализированное соединение будет висеть внутри вашего объекта, и никакого способа избавиться от него нет (если, конечно, вы не реализуете try-catch внутри Expose).

                                                  Как так не нужно оборачивать? Если у вас в конструкторе возникнет исключение, а вы его нигде не обработаете, то приложение упадёт с UnhandledException. Или вы что-то другое имеете в виду?

                                                  А ничего, что TimeoutException бывает не только в таких случаях, и не всегда гарантировано, что соединение закрыто?

                                                  Не помню точного названия исключения, которое возникает при превышении интервала бездействия и автоматическом его разрыве сервером, но оно точно есть и отличается от остальных. Что-то вроде ConnectionHasBeenClosedByServer.

                                                  К сожалению, только неявно. Никакой безопасности в вашей реализации нет, более того, вы с каждой версией порождаете новые потенциальные утечки.

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


                                                  1. lair
                                                    28.04.2015 11:40
                                                    +1

                                                    Это точно такая же ситуация, что и с вызовом метода после Dispose, и ничего в ней нет особенного.

                                                    Да как же нет? Это ситуация, которая порождает исключение времени выполнения, и если ее можно избежать, ее лучше избежать. Решение с using именно это и преследует: в нем нельзя вызвать объект ни до инициализации, ни после деинициализации.

                                                    Нет ничего нерешаемого в данной ситуации, если это достаточно критично для приложения.

                                                    Для серверного приложения (а напомню, мы говорим о сервере и высокой производительности) неотпущенные соединения — это всегда критично. Так что это надо решать.

                                                    С помощью вызовов Expose/Dispose можно более точно контролировать время жизни соединения, если это нужно.

                                                    Зачем может быть нужно открывать соединение раньше вызова GetSomeData?

                                                    Более того так очень легко проверить доступность сервера базы данных (протестировать соединение).

                                                    Зачем это может быть нужно (особенно в данном классе)?

                                                    Как так не нужно оборачивать? Если у вас в конструкторе возникнет исключение, а вы его нигде не обработаете, то приложение упадёт с UnhandledException. Или вы что-то другое имеете в виду?

                                                    Да, я имею в виду «что-то другое». Если в моем случае где-нибудь на два уровня выше будет try-catch, который перехватит ошибку, то незакрытых ресурсов не останется. В вашем случае, при том же расположении try-catch, внутри приложения останется неотпущенное соединение.

                                                    Не помню точного названия исключения, которое возникает при превышении интервала бездействия и автоматическом его разрыве сервером, но оно точно есть и отличается от остальных. Что-то вроде ConnectionHasBeenClosedByServer.

                                                    У вас — на сервере! — настолько много свободных соединений, что вы можете позволить себе держать их до падения по idle?

                                                    Давайте больше конкретики.

                                                    Вся переписка выше — сплошная конкретика. Могу повторить:

                                                    • Утечка при невызванном Dispose
                                                    • Утечка при ошибке инициализации (внутри Expose)
                                                    • Утечка при TimeoutException


                                                    1. Makeman Автор
                                                      28.04.2015 12:15

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

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


                                                      1. lair
                                                        28.04.2015 12:20

                                                        Мою реализацию вы можете использовать точно таким же образом с помощью using, как и свою.

                                                        Это как же? Вот так?

                                                        using(var p = new UserDataProvider())
                                                        {
                                                          p.GetSomeData()
                                                        }
                                                        


                                                        Тогда пропадает состояние, ради которого объект лежал в сессии.

                                                        Если же нужно более тонкое управление поведением установки соединения, а порой возникает и такая необходимость,

                                                        Я вас уже некоторое время прошу привести пример такой необходимости.

                                                        ваша реализация не столь удобна.

                                                        Отнюдь. В моей реализации точно известно, когда открывается соединение, и этот момент полностью контролируется пользователем.

                                                        какой-то сверхсложной логики тут нет, поэтому разберётся даже начинающий разработчик

                                                        Почему, если ничего сложного нет, столько потенциальных утечек?


                                                        1. Makeman Автор
                                                          28.04.2015 12:42

                                                          Извиняюсь, но не обратил внимание на параметр в кострукторе. Подправьте немного и покроется ваш вариант.

                                                              private readonly UserData _userData;
                                                          
                                                              public UserDataProvider(UserData userData)
                                                              {
                                                                _userData = userData;
                                                                 Expose();
                                                              }
                                                          


                                                          Вы, вероятно, придерживаетесь консервативного стиля в программировании, когда объект пишется под конкретный сценарий использования (using). Мне же ближе вариант, когда объект можно использовать в различных сценарях даже в самых неожиданных.

                                                          Я вас уже некоторое время прошу привести пример такой необходимости.

                                                          Да просто протестировать соединение с сервером (иногда бывает такая кнопка в некоторых приложениях). Мне не нравятся такие примеры, но можно динамически переоткрыть соединение в целях информационной безопасности.


                                                          1. lair
                                                            28.04.2015 12:55

                                                            Извиняюсь, но не обратил внимание на параметр в кострукторе. Подправьте немного и покроется ваш вариант.

                                                            После чего выбрасываем UserId и SessionData — в них же больше нет смысла, правильно? — и получаем мое решение. Напомню, что изначально вы ставили задачу следующим образом:
                                                            Пускай есть объект, который достаточно дорог при создании и занимает ресурсы которые нужно переодически освобождать, а также важно его внутреннее состояние. То есть вариант с UOW и пулом не подходит, поскольку внутреннее состояние должно оставаться прежним. Совместное же использование Expose/Dispose решает эту проблему.

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

                                                            Да просто протестировать соединение с сервером (иногда бывает такая кнопка в некоторых приложениях).

                                                            Во-первых, это не имеет никакого отношения к серверному классу. Во-вторых, это нарушение SRP — за тестирования связи с сервером должен отвечать отдельный класс, а не класс, отвечающий за сессионные данные. И, наконец, тестирование связи с сервером — это не контроль времени жизни соединения.

                                                            Так что пример не принят, предложите другой.


                                                            1. Makeman Автор
                                                              28.04.2015 13:09

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


                                                              1. lair
                                                                28.04.2015 13:40

                                                                Пример не может быть «универсальным в плане использования», универсальным может быть решение. То же касается и опасности.

                                                                Так что на данный момент картина, к сожалению, такова, что предложенная вами в начале статьи концепция Renewable Unit, равно как и вообще идея объединять Exposable с Disposable, критики не выдерживают — ровно потому, что единственная приведенная вами задача, где это, по вашему мнению, нужно, в итоге даже вами была решена не так.


                                                                1. Makeman Автор
                                                                  28.04.2015 23:20

                                                                  Всё же не разделяю ваше мнение. Пускай каждый сам взвесит все за и против и решит, что для него ближе.

                                                                  По крайней мере существует сценарий совместного использования Expose/Dispose, который уже тесно перекликается со второй частью статьи. Например, дата-контракт сериализаторы, используемые для хранения состояния вью-моделей, не вызывают конструктора при создпании объекта (см. FormatterServices.GetUninitializedObject). Собственно из этого и родился паттерн Exposable. Если же ещё вью-модель имеет дело с unmaged-ресурсами, то очень логично использовать Dispose-метод, как минимум, в финализаторе. Но это уже спецефический случай, поэтому не привожу его в качестве примера.


                                                                  1. lair
                                                                    28.04.2015 23:45

                                                                    Всё же не разделяю ваше мнение.

                                                                    В данном случае речь идет не о мнении, а об аргументах.

                                                                    По крайней мере существует сценарий совместного использования Expose/Dispose

                                                                    Вы так и не смогли обосновать его необходимость.

                                                                    Например, дата-контракт сериализаторы, используемые для хранения состояния вью-моделей, не вызывают конструктора при создпании объекта

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

                                                                    Собственно из этого и родился паттерн Exposable.

                                                                    Это не «Exposable». Это разделение создания и инициализации. К повторной инициализации это никакого отношения не имеет.


                                                                    1. Makeman Автор
                                                                      29.04.2015 11:28

                                                                      В данном случае речь идет не о мнении, а об аргументах.

                                                                      Ваши аргументы не убедили меня в том, что комбинированный вызов Expose/Dispose плох и его не стоит применять.

                                                                      Вы так и не смогли обосновать его необходимость.

                                                                      Но и вам также не удалось обосновать его несостоятельность. Считаю, что наличе альтернативных решений — это плюс, поэтому не будем навязывать своё мнение окружающим, а пусть каждый для себя выбирает наиболее привлекательный вариант :)


                                                                      1. lair
                                                                        29.04.2015 11:30

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


                                                                        1. Makeman Автор
                                                                          29.04.2015 11:40

                                                                          Используйте try-finally и никаких утечек точно не будет. Ровно также можно сказать, что в Disposble-паттерне открытые утечки, если не обернуть его в using, а ведь бывает и так! Но это же не повод не применять Disposable.


                                                                          1. lair
                                                                            29.04.2015 14:23
                                                                            +1

                                                                            Disposable расчитан на короткоживущие объекты. Ваш сценарий — на долгоживущие. Соответственно, опасность утечки «до GC» в вашем случае больше.


                                                                            1. Makeman Автор
                                                                              29.04.2015 16:29

                                                                              Соглашусь с этим :)


                                    1. Makeman Автор
                                      27.04.2015 19:27

                                      В крайнем случае попробуйте использовать только один метод Expose, например, во вью-моделях, как описано в статье. Он помогает очень красиво организовать код и реализовать циклические инжекции.


                                      1. lair
                                        27.04.2015 20:50

                                        Мне не интересно обсуждать вью-модели и однократную инициализацию, я для этого использую DI. Я обсуждаю конкретный пример (и подход), предложенный вами в первой части статьи.


                        1. Makeman Автор
                          27.04.2015 16:41

                          В любом случае хочу поблагодарить вас за дискуссию! Благодаря ей удалось более строго и чётко сформулировать мысли, изложенные в статье.


                  1. areht
                    29.04.2015 16:53
                    +1

                    > Так почему бы не расширить понимание паттерна Disposable?

                    Это называется «натянуть сову на глобус». «В этом есть красота», понимаю.

                    Если у вашего объекта есть 2 состояния, в одном можно вызвать только Expose, во втором всё остальное, кроме Expose — это вы, по определению, сложили 2 объекта в один.


                    1. Makeman Автор
                      29.04.2015 17:02

                      С чувством юмора у вас всё в порядке, так держать :)

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


                      1. areht
                        29.04.2015 17:22

                        ок, пойдём длинным путём.

                        Что такое объект? Как вы решаете какие переменные и методы принадлежат одному объекту?


                        1. Makeman Автор
                          29.04.2015 21:20

                          Вы уверены, что хотите лезть в такие абстрактные дебри?

                          Что такое объект?

                          Этому понятию можно дать множество определений… Счётная конструкция, единица логического мышления, обладающая некоторым набором атрибутов. Множества объектов с аналагичным набором атрибутов образуют классы. Вот, кстати лекция интересная по безатрибутным моделям данных www.youtube.com/watch?v=Ioi68tNLmYw.
                          Ещё можете статью прочитать habrahabr.ru/post/250135.

                          Как вы решаете какие переменные и методы принадлежат одному объекту?

                          Объекты со сходным набором атрибутов и поведением, относящиеся к одному классу, по определению обладают одними и теми же методами и свойствами.


                          1. areht
                            30.04.2015 00:33

                            > Объекты со сходным набором атрибутов и поведением, относящиеся к одному классу, по определению обладают одними и теми же методами и свойствами.

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

                            Вопрос был именно про агрегацию переменных и методов в «Объекты со сходным набором атрибутов и поведением»


          1. FiresShadow
            27.04.2015 12:00
            +3

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


            Скорее всего, ощутимого выигрыша в производительности не будет. Основная нагрузка на сборщик мусора ложится после вызова context.Dispose(), который освобождает кэшированные результаты выполнения запросов и некоторые другие ресурсы. При этом нагрузка на сборщик мусора при сборе непосредственно самих ссылок на context не такая уж и существенная. Когда речь заходит об оптимизации, нужно быть осторожным: экономия на спичках не даёт заметного выигрыша в производительности (пользователь даже не заметит — была проведена оптимизация или нет), но такие оптимизации могут сделать код более запутанным и неочевидным.
            Кроме того, напомню, зачем вообще нужен сборщик мусора: он освобождает программиста от заботы о том, в какой момент времени нужно освободить выделенную память. Если требования к быстродействию достаточно высоки и есть необходимость отслеживать время жизни объектов в каждой функции, то возможно следует воспользоваться языком программирования без сборщика мусора. Хотя, скорее всего, дешевле и проще было бы поставить более мощный сервер или, в данном случае, рассмотреть вариант использования какой-нибудь легковесной ORM без внутреннего кэша.


            1. Makeman Автор
              27.04.2015 16:06

              Соглашусь с вашими рассуждениями. Вероятно, подобрал не самый удачный пример для иллюстрации паттерна. Взгляните на вышестоящий комментарий habrahabr.ru/post/256629/#comment_8395113, чтобы лучше уловить суть того, что мне хотелось сказать.


  1. mird
    26.04.2015 19:53
    +1

    Интересно, что вначале вы предлагаете использовать вместе IExposable и IDisposable, однако в примерах этого самого IExposable ни одна из вью моделей не Disposable. Не по той ли причине, про которую пишет выше lair?


    1. Makeman Автор
      26.04.2015 22:49

      В примерах просто не возникло необходимости в IDisposable. Хотя вполне могут иметь место сценарии, где вью-модель занимает какие-то ресурсы, которые нужно освобождать.

      Exposable и Disposable применимы как раздельно, так и совместно — зависит от ситуации. Внимания заслуживает тот факт, что оба паттерна обратно симметричны относительно жизненного цикла объекта.


      1. areht
        29.04.2015 16:39
        +1

        > Внимания заслуживает тот факт, что оба паттерна обратно симметричны относительно жизненного цикла объекта.

        Всё таки хорошо когда человек имеет твердую жизненную позицию в вопросе «шашечки или ехать».


  1. ap3rus
    26.04.2015 23:30
    +1

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


    1. Makeman Автор
      27.04.2015 10:06

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

      А если у вас циклические ссылки не двух, а трёх и более классов друг на друга?
      Механизм Exposable позволяет красиво разрешить и такие ситуации.


      1. areht
        29.04.2015 16:44
        +1

        > А если у вас циклические ссылки не двух, а трёх и более классов друг на друга?

        Значит у меня сильная связность кода и я что-то делаю очень неправильное.


        1. Makeman Автор
          29.04.2015 16:57

          Вы так думаете, потому что привыкли к такому стилю программирования.

          В человеческом теле множество органов — каждый выполняет свою функцию (Single Responsibility), но вот друг без друга они функционировать не могут. Скажете, сильная связность, природа и миллионы лет эволюции плохой архитектор?

          В окружающем мире множество аналогий, которые можно перенести в программирование. Возможно, вы посмеётесь над этим, но в устройстве мира достаточно красоты и стройности.


          1. areht
            29.04.2015 17:03

            > Скажете, сильная связность, природа и миллионы лет эволюции плохой архитектор?

            Вообще говоря плохой, но, пока при рождении мама не должна каждый орган за Expose подергать, я за природу спокоен.


            1. Makeman Автор
              29.04.2015 17:05

              Конструктивно!


              1. areht
                29.04.2015 17:14
                +1

                Ну не обсуждать же с вами всерьёз сколько ссылок имеют органы и сколько курица живёт без головы.

                Аргумент «я художник, я так вижу» вполне самодостаточен, что тут обсуждать?


                1. Makeman Автор
                  29.04.2015 21:24

                  Да, художник, вы чертовски правы. И для меня программирование — и ремесло, и искусство. А вдохновение — окружающий мир.


          1. lair
            29.04.2015 17:14
            +1

            Скажете, сильная связность, природа и миллионы лет эволюции плохой архитектор?

            А вы по каким-то причинам считаете, что у человеческого организма и программного комплекса одинаковые нефункциональные критерии?

            Как известно, сильная связность — это противник поддерживаемости, изменяемости и тестируемости. А вот теперь скажите мне, насколько легко человеческий организм модифицируем и тестируем?


            1. Makeman Автор
              29.04.2015 21:43

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

              Но вы, конечно же, имеете ввиду внешнее вмешательство… Но тогда вам другой пример — автомобиль.

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


              1. lair
                29.04.2015 21:48
                +1

                Изменяемость и подстраиваемость под внешние условия на очень высоком уровне.

                И давно вы можете без вреда для организма пить соленую воду?

                По вашей логике — в данной системе высокая связность,

                Это не по моей логике, это по логике, которую вы мне приписываете. А по моей логике, какие-то части в машине связаны сильнее, чем надо (в частности, бортовая электроника), а какие-то — адекватно (например, вы можете заменить колеса на другие, подходящие по стандарту; и те же колеса, кстати, ничего не знают о машине, к которой они прикручены).

                Аналогии врут. А аналогии из предметного мира в абстрактном мире врут просто безбожно. И да, любому предмету из материального мира далеко до тех нефункциональных требований, которые предъявляют к современному ПО. Вы наверняка неоднократно видели статьи класса «что было бы, если бы дома строили так, как пишут программы» — вот там хорошо про это сказано.


                1. Makeman Автор
                  01.05.2015 13:36

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


                  1. lair
                    01.05.2015 14:08

                    А так долго время и пытались делать — собственно, термин software engineering отсюда и происходит. Получается, скажем так, не всегда подходяще. У МакКоннелла это все описано, аккурат в главе про метафоры программирования.


              1. areht
                30.04.2015 00:48

                > Поддерживается и тестируется организм достаточно легко, а бонусом является мощнейшая система самодиагностики и восстановления.

                Вы когда-нибудь слышали про рак, аутоимунные заболевания, аллергию и дерматиты, побочные эффекты лекарств?

                > Есть, конечно, второстепенные компоненты, которые можно выбросить, но в целом даже без одного колеса это неполноценный автомобиль.

                По моей логике у колесо автономно и самодостаточно


  1. Burantino
    27.04.2015 10:37
    +1

    День добрый! Там похоже очепятка в примере кода:

    var productsViewModel = Store.Get<SettingsViewModel>();
    



     Store.Get<ProductsViewModel> 
    

    должно же быть или я ничего не понял? :)


    1. Makeman Автор
      27.04.2015 10:40

      Да, спасибо! Вкралась опечатка. Уже исправил.


  1. nickolaym
    27.04.2015 14:50

    А это, извините, не старый добрый пул объектов или флайвейт?


    1. Makeman Автор
      27.04.2015 15:17

      Не похоже на эти паттерны. Посмотрите habrahabr.ru/post/256629/#comment_8395113


  1. Vadimyan
    29.04.2015 11:55
    +1

    Честно сказать, я не очень люблю читать комментарии lair к статьям по .net, поскольку считаю его безальтернативным скептиком («У вас всё плохо, но как хорошо я не скажу») но в данном случае я вынужден с ним согласиться. Здесь описан какой-то кошмар :)
    Первым делом, что касается инициализации и применения сомнительного дополнительного метода, то это классическая проблема, решаемая при помощи автофабрик. Для Unity такой прелести нет, но на самом деле есть. И всё это можно получить без сомнительного ServiceLocator в лице Store. У вас ещё и тестируемость усложняется, поскольку внешние зависимости инжектируются мимо конструктора.
    Что касается умного состояния и его сохранения. Наследование? Ни за что. В тот момент, когда вам будет необходимо отнаследоваться от стороннего класса и иметь умное состояние — всё рухнет. Вот тут есть небольшая попытка осмысления сохранения настроек, есть и гораздо более развитые библиотеки для этого, работающие на основе атрибутов [StorableSetting], которые позволяют совсем не писать код сохранения и легко разделяют сохраняемые и несохраняемые поля. Серьезно, только представьте, достаточно пометить свойство атрибутом. А сохранение и восстановление встраивается в механизм навигации, т.е. при создании View/ViewModel. Не забудьте только про рекурсивный обход объектов, состояние дочерних ViewModel и их свойств тоже нужно сохранять и восстанавливать.
    Глобально, мне не нравится, что Exposable предлагает писать код сверх нужного. Нужно имплементировать Expose(), то есть аналог Initialize(). До вызова этого метода объект непригоден к работе, то есть нужно делать дополнительные проверки того, что объект уже инициализирован. Если встроить поддержу Exposable в using(), то сама необходимость его не совсем ясна. Вызывать Expose() в конструкторе? Чем он тогда будет лучше самого конструктора? У вас поддержка на уровне библиотеки через Store, это как вызов в конструкторе, только можно забыть и инициализировать объект второй раз явно. Или где-то он будет создаваться через new и окажется не инициализирован. Я понимаю, контракты, но зачем усложнять то, что и так просто сделать. Мне интересен реальный пример, оправдывающий такой overhead. Он у вас есть? Из проекта, из кода, чтобы без Exposable было сложнее, чем с ним.
    Не стоит пытаться спорить с lair, достаточно просто сравнить его статьи и комментарии.


    1. Makeman Автор
      29.04.2015 14:03
      +1

      Мне интересен реальный пример, оправдывающий такой overhead. Он у вас есть? Из проекта, из кода, чтобы без Exposable было сложнее, чем с ним.

      Напишите аналогичный простенький текстовый редактор с помощью любых предложенных вами библиотек и сравните количество и читаемость получившегося кода. Не забудьте сохранить состояние как логическое (вкладки, текст), так и визуальное (размеры и положения окна, настройки шрифта, цвета). В более сложных проектах выигрыш получится ещё намного выше.

      Что касается наследования, то в подавляющем большинстве случаев вью-модели наследуются от BaseViewModel, как минимум, реализующей INotifyPropertyChanged, поэтому касательно этого вопроса критика не обоснована. Атрибуты плохи для сохранения UI-настроек, поскольку в сложных интерфейсах могут быть десятки свойств, а неявное их сохранение значительно лаконичнее и проще. Пример такого проекта — текстовый редактор Poet, можно взглянуть на служебные файлы с настройками и убедиться, что их там действительно много.

      Насчёт инжекций в конструктор — долгий вопрос. Во-первых, конструктор часто не вызывается при десериализации. Во-вторых, стоит убрать или добавить параметр в конструктор, как ломаются тесты тех методов, которые вообще не связаны с этой логикой. Меня это очень бесило на некоторых проектах :) О каком удобстве тестирования тут можно говорить?

      Если с какими-то пунктами критики по первой части (Renewable Unit) ещё соглашусь, то по второй у меня очень много аргументов в запасе. Тут нужно просто прочувствовать на практике насколько удобно всё организовано в предложенном фреймворке.


      1. Vadimyan
        29.04.2015 14:44
        +1

        Не забудьте сохранить состояние как логическое (вкладки, текст), так и визуальное (размеры и положения окна, настройки шрифта, цвета).

        Вау, похоже на ту самую задачу, которую уже давно решили. Это не просто ссылки, механизм DX действительно использовался наряду с [StorableSetting]. DX сохранял данные о положении окон в MDI-приложении (без написания дополнительного кода, в данном случае мы помечали View в View-first подходе пустым интерфейсом ради удобства), а дальше уже рекурсивно восстанавливались настройки дочерних View и ViewModel. Можно было переключаться в режим локального хранения настроек или хранения в БД для пользователя. Для сравнени с текстовым редактором в системе было 10^7 строк кода. Пометить интерфейсом View, добавить атрибуты, всё. При необходимости можно использовать методы BeforeRestoreLayout, AfterRestoreLayout для кастомизации, которая была нужна в 5-10 случаях. Можно через StorableSetting проводить явную инициализацию формы при вызове, но это уже несколько другая история.

        Соответственно, довод о неявном сохранении большого количества настроек здесь неактуален — для визуального состояния всё так и есть. А в логическом для крупных систем всё может быть на столько запутано, что стоит ли надеется на неявное сохранение?
        Может быть не стоит сериализовать (и вообще делать Serializable) вьюмодели и проблемы с вызовом конструктора с параметрами исчезнут? Со сломанными тестами вопрос спорный, в моём случае тесты не соберутся, в вашем упадут при прогоне (т.к. объект окажется недоинициализирован) с NullReferenceException. Тут уж каждому своё.

        Я не вижу смысла спорить. Я верю, что для ваших задач ваше решение подходит, а руки уже согнулись в нужную сторону для применения описанных техник. Мои руки согнуты в другую сторону. И сломаны в нескольких местах. И поменяны местами. И необязательно друг с другом.

        Просто собственный Application framework это всегда что-то текучее и нужно гнуть его под задачи непрерывно, а не наоборот.


        1. Makeman Автор
          29.04.2015 16:26
          +1

          Вы думаете, что автору не доводилось видеть подобных решений до того момента, как делать другое? К слову AvalonDock, который используется в Poet, работает схоже, но ведь не на пустом месте возникли мысли сделать иначе. В ваших примерах сохранение и загрузка layout встроены в сам контрол, но как вы поступите, если у вас есть другой готовый контрол, который такой функциональности не поддерживает?

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

          Фреймворк прекрасно подходит для любых xaml-ориентированных приложений и покрывает очень большой круг задач.


    1. Makeman Автор
      29.04.2015 14:10

      Что ещё немаловажно, текущее решение является кроссплатформенным и вью-модели запросто разделяются между различными проектами, причём с различным UI. Насколько видоизменится ваше решение, если добавить ещё и такое условие?