Является ли это дырявой абстракцией, если реализация интерфейса вызывает Dispose

Рассмотрим этот код:

public class MyClass()
{
  public MyClass()
  {    
  }

  public DoSomething()
  {
    using (var service = new CustomerCreditServiceClient())
    {
       var creditLimit = service.GetCreditLimit(
         customer.Firstname, customer.Surname, customer.DateOfBirth);       
    }
  }
}

Теперь мы хотим реорганизовать его, чтобы слабо связать его. Мы заканчиваем это:

public class MyClass()
{
  private readonly ICustomerCreditService service;

  public MyClass(ICustomerCreditService service)
  {
     this.service= service;
  }

  public DoSomething()
  {
     var creditLimit = service.GetCreditLimit(
       customer.Firstname, customer.Surname, customer.DateOfBirth);       
  }
}

Выглядит нормально, верно? Теперь любая реализация может использовать интерфейс и все хорошо.

Что, если я сейчас скажу, что реализация представляет собой класс WCF и что оператор using до того, как был выполнен рефакторинг, существовал не просто так. т.е./закрыть соединение WCF.

Итак, теперь наш интерфейс должен реализовать вызов метода Dispose, или мы используем фабричный интерфейс, чтобы получить реализацию и поместить вокруг нее оператор using.

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

Может ли кто-нибудь помочь мне понять это и подтвердить, прав я или нет.

Спасибо


person Jon    schedule 04.09.2012    source источник
comment
Вы можете сделать так, чтобы ваш интерфейс наследовался от Dispose   -  person Thomas Levesque    schedule 04.09.2012
comment
True или реализуйте собственный Dispose. В любом случае, вызов Dispose — это утечка, не так ли?   -  person Jon    schedule 04.09.2012


Ответы (5)


Да, это дырявая абстракция, когда вы позволяете ICustomerCreditService реализовать IDisposable, поскольку теперь вы написали ICustomerCreditService с учетом конкретной реализации. Кроме того, это сообщает потребителю этого интерфейса, что он может удалить эту услугу, что может быть неправильным, тем более, что в общем случае ресурс должен быть удален тем, кто его создает (кто имеет право собственности). Когда вы вводите ресурс в класс (например, используя внедрение конструктора), неясно, передается ли право собственности потребителю.

Так что, как правило, тот, кто отвечает за создание этого ресурса, должен его утилизировать.

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

public class WcfCustomerCreditServiceClient
    : ICustomerCreditServiceClient
{
    public CreditLimit GetCreditLimit(Customer customer)
    {
        using (var service = new CustomerCreditServiceClient())
        {
            return service.GetCreditLimit(customer.Firstname,
                customer.Surname, customer.DateOfBirth);       
        }
    }
}
person Steven    schedule 04.09.2012
comment
Интересная идея. Единственное, что я бы сказал, это то, что есть еще один уровень абстракции, но это может быть не так уж и плохо само по себе. - person Jon; 04.09.2012
comment
@Jon: Нет нового уровня абстракции. Вы уже определили эту ICustomerCreditServiceClient абстракцию. Разница в том, что ваш прокси-сервер WCF не реализует его, но новые абстракции не добавляются. Выполнение подобных действий предотвращает усложнение конфигурации IoC. - person Steven; 04.09.2012
comment
Возможно, моя терминология неверна. Я имел в виду другой уровень, поэтому MyClass вызывает WcfCustomerCreditServiceClient, который вызывает CustomerCreditServiceClient. - person Jon; 04.09.2012
comment
@Джон: Верно. Но будет ли это когда-нибудь проблемой? - person Steven; 04.09.2012
comment
Если это единственная функция, вызываемая в этом интерфейсе, это также была моя первая идея. Однако, когда есть не только один вызов интерфейса, а несколько (с исходным блоком использования), это может стать медленным, поскольку каждый вызов подключается и отключается... - person FrankB; 04.09.2012
comment
@FrankB: Это правда. Когда с производительностью возникнут проблемы, вам нужно будет увеличить область действия ресурса. Один шаг вперед - использовать фабрику (как вы написали в своем ответе). Следующим шагом является предоставление контейнеру DI/IoC его удаления. Например, при работе в веб-среде вы можете зарегистрировать его как образ жизни «для каждого веб-запроса», что означает, что за время существования одного веб-запроса создается только один экземпляр. Как объясняет Деннис Трауб, если вы работаете не в веб-среде, вам, вероятно, потребуется некоторая область видимости. - person Steven; 04.09.2012
comment
Хотя фабрики в целом являются хорошим решением (и за ними легко следовать), они вынуждают вас иметь еще одну абстракцию в вашей системе. Так что в этом случае я обычно начинаю с реализации оболочки в своем ответе, и если это недостаточно быстро, я перехожу к образу жизни «на каждый веб-запрос» или «срок жизни», как объясняет Деннис. - person Steven; 04.09.2012

Вы должны вызвать Dispose ICustomerCreditService там, где он был создан, поскольку MyClass теперь не знает о жизненном цикле ICustomerCreditService.

person Ignacio Soler Garcia    schedule 04.09.2012
comment
Но все это будет сделано с контейнером IOC, поэтому вам нужно будет что-то указать для этого. Я думаю, что Ninject позволяет вам вызывать удаление для реализации зависимости при ее настройке. - person Jon; 04.09.2012

Начиная снова с первой реализации, я бы попытался добавить в класс getInterface-Request, чтобы реализация могла оставаться более или менее такой же. Затем он может безопасно вызывать Dispose (фактически он только откладывает создание реализации интерфейса, но сохраняет контроль над его жизненным циклом): (c#-код не проверен...)

public class MyClass()
{
  public delegate ICustomerCreditService InterfaceGetter;
  private InterfceGetter getInterface;
  public MyClass(InterfaceGetter iget)
  {
    getInterface = iget;
  }
  public DoSomething()
  {
    using (var customerCreditService = getInterface())
    {
       var creditLimit = customerCreditService.GetCreditLimit(customer.Firstname, customer.Surname, customer.DateOfBirth);       
    }
  }
}
person FrankB    schedule 04.09.2012
comment
Я нахожу ваш ответ немного расплывчатым, но вы в основном советуете внедрить фабрику, которая позволяет создать этот экземпляр (+1 за это). В общем, фабрики хороши, так как ясно, что вызывающая сторона получает право собственности на созданный экземпляр, и ясно, что он должен избавиться от этого экземпляра. Однако вам все равно нужно реализовать IDisposable на ICustomerCreditService, что в данном случае не требуется. - person Steven; 04.09.2012

Вы должны обрабатывать жизненный цикл customerCreditService в вызывающем коде. Как MyClass узнать, нужна ли услуга вызывающему абоненту? Если вызывающий объект отвечает за очистку своих ресурсов, то MyClass не обязательно должен быть одноразовым.

// calling method

using (var service = new CustomerCreditServiceClient()) {
    var myClass = new MyClass(service);
    myClass.DoSomething();
}

Обновление: В комментариях OP упомянул использование IoC, такого как Ninject. Тогда код может выглядеть так:

IKernel kernel = ...;

using (var block = kernel.BeginBlock())
{
    var service = block.Get<ICustomerCreditService>();
    var myClass = new MyClass(service);
    myClass.DoSomething();
}

kernel.BeginBlock() создает блок активации. Это гарантирует, что разрешенные экземпляры удаляются, когда блок заканчивается.

person Dennis Traub    schedule 04.09.2012
comment
Но все это будет сделано с контейнером IOC, поэтому вам нужно будет что-то указать для этого. Я думаю, что Ninject позволяет вам вызывать удаление для реализации зависимости при ее настройке. - person Jon; 04.09.2012
comment
Затем вы можете разрешить контейнер IoC и избавиться от файла CustomerCreditServiceClient. По-прежнему нет необходимости реализовывать IDisposable.Dispose() в MyClass. - person Dennis Traub; 04.09.2012
comment
Вы бы не передавали ядро ​​так - person Jon; 04.09.2012
comment
Это просто упрощенный пример. Вы никогда не упоминали контейнер IoC, и я просто попытался расширить свой ответ в соответствии с вашим комментарием. Пожалуйста, обновите свой вопрос, если использование контейнера так важно. OTOH: Использование контейнера IoC само по себе считается антишаблоном. - person Dennis Traub; 04.09.2012

Да это так. Но это необходимое зло. Само существование интерфейса IDisposable представляет собой дырявую абстракцию. Дырявые абстракции — это просто повседневный факт программирования. По возможности избегайте их, но не беспокойтесь, если не можете — они все равно вездесущи.

person Konrad Rudolph    schedule 04.09.2012
comment
@FrankB Хм… не могли бы вы уточнить? Ведь это не мнение, с которым можно согласиться или не согласиться, это факт — с фактами нельзя не согласиться, только их оценка. Если вы считаете, что я ошибся в фактах или их оценке, то, пожалуйста, скажите мне об этом, и я это исправлю. - person Konrad Rudolph; 04.09.2012
comment
@Стивен То же самое для тебя. См. предыдущий комментарий. Между прочим, я только что видел ваш ответ, и это действительно надежный способ справиться с этим конкретным случаем. Хотя мой ответ был более общим. - person Konrad Rudolph; 04.09.2012
comment
@KonradRudolph IDisposable предназначен именно для того, чтобы не было протечек. Если вы имеете в виду, что это легко сделать неправильно; тогда да, это плохо. Но это не вина IDisposables... Это чистый способ держать под контролем неуправляемые вещи. кстати: вы не уточнили, что делает его негерметичным? - person FrankB; 04.09.2012
comment
@FrankB Это не то, что означает «дырявая абстракция». Это не имеет ничего общего с утечкой ресурсов. Дырявая абстракция означает утечку сведений о реализации (таких как: «этот ресурс необходимо удалить вручную») в общедоступный интерфейс. - person Konrad Rudolph; 04.09.2012
comment
@KonradRudolph хорошо, понял. Тем не менее, я все еще не могу полностью согласиться с вашим общим утверждением... (но это не по теме этого вопроса) - person FrankB; 04.09.2012
comment
IDisposable сам по себе не является дырявой абстракцией. IDisposable — это четкий контракт, который сообщает, что реализующий его тип имеет ресурсы, которые можно детерминистически очистить. Однако, когда вы применяете IDisposable к другому интерфейсу, этот интерфейс может стать негерметичной абстракцией. Причина в том, что невозможно предусмотреть все возможные реализации интерфейса, и поэтому всегда можно придумать одноразовую реализацию практически любого интерфейса. - person Steven; 04.09.2012
comment
Когда вы реализуете IDisposable в интерфейсе, вы передаете знания о реализации через дизайн интерфейса. Однако когда вы реализуете IDisposable в реализации, у вас нет дырявой абстракции. Вы просто заявляете, что эта реализация управляет ресурсами. Следовательно, IDisposable сама по себе не является дырявой абстракцией, и поэтому я с вами не согласен. - person Steven; 04.09.2012
comment
@Steve Спасибо, что прояснили это. Но это оставляет нас в интересной ситуации, поскольку я утверждаю, что да, IDisposable является всегда дырявой абстракцией на самом фундаментальном уровне. Он сообщает что-то об объекте, для которого пользователь объекта не имеет ни малейшего значения; а именно, что это требует явного управления ресурсами. Это не назначение объекта, это неудобная деталь. - person Konrad Rudolph; 04.09.2012
comment
@KonradRudolph: пользователь абстракции не заботится о IDisposable (поэтому интерфейс не должен реализовывать IDisposable), но пользователь реализация заботится о IDisposable, поскольку он отвечает за его создание и удаление. Утечка в абстракции означает, что детали реализации просачиваются через абстракцию. Однако, когда вы внедряете IDisposable в реализацию, утечки знаний нет; просто сообщаю то, что нужно. - person Steven; 04.09.2012
comment
@Steve Если я использую System.Drawing.Graphics, то я пользователь этого класса. Тем не менее, этот класс реализует (должен реализовать) IDisposable. Таким образом, Graphics раскрывает подробности о своей внутренней реализации (с точки зрения GDI+ операционной системой, где дескрипторы должны быть выделены и удалены). - person Konrad Rudolph; 04.09.2012
comment
@KonradRudolph: Тот факт, что некоторые объекты нуждаются в детерминированном удалении, является неотъемлемой частью того, как работает .NET (или, возможно, даже любая компьютерная система). Если вы примете во внимание эту абстракцию (над аппаратным обеспечением), то да; это действительно дырявая абстракция. В этом случае само управление ресурсами является абстракцией, а я говорил о ICustomerCreditService как об абстракции. Вывод: мы оба были правы :-) - person Steven; 04.09.2012
comment
@KonradRudolph У вас почти есть я, но чтобы перевернуть это с ног на голову: у вас может быть реализация, в которой Dispose ничего не делает (потому что этой реализации это не нужно). В этом случае пользователь думает, что знает что-то о реализации, хотя на самом деле этого не знает. IDisposable просто просит пользователя сообщить об этом, когда он закончит. Разве это не больше похоже на поведение (лучшего термина я не нашел), чем на утечку? - person FrankB; 04.09.2012
comment
@Steve Да, управление ресурсами является дырявой абстракцией. Джоэл Спольски написал на эту тему основополагающее эссе, в котором заключает: «Все нетривиальные абстракции в какой-то степени негерметичны». - и он совершенно прав. - person Konrad Rudolph; 04.09.2012