Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Нет, не замечаю. Я, наоборот, замечаю крупное семантическое расхождение.
В том-то и дело, что Dispose — это не деинициализация, это полный отказ от объекта.

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

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

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

Зачем? Чем вас не устраивает object pool, в котором все уже реализовано?
Мой ответ, наверняка, вас не устроит — в этом есть красота :)
По крайней мере, вижу её там очень чётко.

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

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

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

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

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

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

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

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

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

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

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

Банально, ведь так работает телевизор или компьютер. Для быстрого включения, сохранения состояния (открытые приложения) и экономии ресурсов используется спящий режим. Почему не перенести такую аналогию в программирование, ведь это тоже своего рода паттерн проектирования бытовой и вычислительной техники…
Эта задача тривиально решается разделением хранимого состояния и провайдера данных. Объект, отвечающий за состояние, живет в сессии, а провайдер порождается на его основании по запросу. Разделение ответственности в действии.
Или тривиально решается при помощи Expose/Dispose с тем же разделением ответственности, но только провайдер не создаётся заново каждый раз, а выходит из режима ожидания. Просто альтернативные способы со своими плюсами и минусами. И каждый имеет право на жизнь.
У вас нет разделения ответственности при использовании Expose/Dispose, потому что один и тот же класс отвечает и за долгоживущие, и за короткоживущие данные. Что хуже, вы нарушаете привычное для пользователя поведение Dispose, при котором после вызова Dispose к объекту обращаться не надо.
Почему нет разделения ответственности? Всё так же, как и у вас, единственное отличие в том, что вместо new Provider(ConnectionString) будет _provider.Expose(). Если вам привычнее классический вариант, то используйте его, или назовите методы по-другому, суть паттерна останется прежней.

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

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, где и будет радостно собран вместе со всеми его зависимостями (включая вызов финализатора, если тотопределен)
Если внутри конструктора будет эксепшн, объект сразу попадет в очередь 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();
            }
        }
    }


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

    private readonly UserData _userData;

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Это не «Exposable». Это разделение создания и инициализации. К повторной инициализации это никакого отношения не имеет.
В данном случае речь идет не о мнении, а об аргументах.

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

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

Но и вам также не удалось обосновать его несостоятельность. Считаю, что наличе альтернативных решений — это плюс, поэтому не будем навязывать своё мнение окружающим, а пусть каждый для себя выбирает наиболее привлекательный вариант :)
Понимаете ли, в чем дело… открытые утечки ресурсов — это уже достаточный аргумент не применять ваш подход.
Используйте try-finally и никаких утечек точно не будет. Ровно также можно сказать, что в Disposble-паттерне открытые утечки, если не обернуть его в using, а ведь бывает и так! Но это же не повод не применять Disposable.
Disposable расчитан на короткоживущие объекты. Ваш сценарий — на долгоживущие. Соответственно, опасность утечки «до GC» в вашем случае больше.
Соглашусь с этим :)
В крайнем случае попробуйте использовать только один метод Expose, например, во вью-моделях, как описано в статье. Он помогает очень красиво организовать код и реализовать циклические инжекции.
Мне не интересно обсуждать вью-модели и однократную инициализацию, я для этого использую DI. Я обсуждаю конкретный пример (и подход), предложенный вами в первой части статьи.
В любом случае хочу поблагодарить вас за дискуссию! Благодаря ей удалось более строго и чётко сформулировать мысли, изложенные в статье.
> Так почему бы не расширить понимание паттерна Disposable?

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

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

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

Что такое объект? Как вы решаете какие переменные и методы принадлежат одному объекту?
Вы уверены, что хотите лезть в такие абстрактные дебри?

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

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

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

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

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

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


Скорее всего, ощутимого выигрыша в производительности не будет. Основная нагрузка на сборщик мусора ложится после вызова context.Dispose(), который освобождает кэшированные результаты выполнения запросов и некоторые другие ресурсы. При этом нагрузка на сборщик мусора при сборе непосредственно самих ссылок на context не такая уж и существенная. Когда речь заходит об оптимизации, нужно быть осторожным: экономия на спичках не даёт заметного выигрыша в производительности (пользователь даже не заметит — была проведена оптимизация или нет), но такие оптимизации могут сделать код более запутанным и неочевидным.
Кроме того, напомню, зачем вообще нужен сборщик мусора: он освобождает программиста от заботы о том, в какой момент времени нужно освободить выделенную память. Если требования к быстродействию достаточно высоки и есть необходимость отслеживать время жизни объектов в каждой функции, то возможно следует воспользоваться языком программирования без сборщика мусора. Хотя, скорее всего, дешевле и проще было бы поставить более мощный сервер или, в данном случае, рассмотреть вариант использования какой-нибудь легковесной ORM без внутреннего кэша.
Соглашусь с вашими рассуждениями. Вероятно, подобрал не самый удачный пример для иллюстрации паттерна. Взгляните на вышестоящий комментарий habrahabr.ru/post/256629/#comment_8395113, чтобы лучше уловить суть того, что мне хотелось сказать.
Интересно, что вначале вы предлагаете использовать вместе IExposable и IDisposable, однако в примерах этого самого IExposable ни одна из вью моделей не Disposable. Не по той ли причине, про которую пишет выше lair?
В примерах просто не возникло необходимости в IDisposable. Хотя вполне могут иметь место сценарии, где вью-модель занимает какие-то ресурсы, которые нужно освобождать.

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

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

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

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

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

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

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

Аргумент «я художник, я так вижу» вполне самодостаточен, что тут обсуждать?
Да, художник, вы чертовски правы. И для меня программирование — и ремесло, и искусство. А вдохновение — окружающий мир.
Скажете, сильная связность, природа и миллионы лет эволюции плохой архитектор?

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

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

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

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

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

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

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

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

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

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

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

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



 Store.Get<ProductsViewModel> 

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

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

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

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

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

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

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

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

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

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

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

Публикации

Изменить настройки темы

Истории