вторник, 7 апреля 2009 г.

“Парные” методы

Значение термина, вынесенного в заголовок, скорее всего никому кроме меня неизвестно. Это потому, что я сам его придумал, так как не знаю точного названия того, о чём хочу рассказать. А речь пойдёт о методах, которые обязательно должны быть вызваны вместе. Примерами таких служат ListBox.BiginUpdate() и ListBox.EndUpdate() или CodeAccessPermission.Assert() и CodeAccessPermission.RevertAssert().

Объединяет эти методы то, что вызвав первый из них (дальше я буду называть его Begin-вызовом) программист обязан (в случае, если вызов завершился успешно) вызвать и второй (я буду называть его End-вызов). Часто реализуют такие вызовы следующим образом:

listBox.BeginUpdate(); // Begin-вызов
try {
  // Некоторая полезная работа
  listbox.Items.Add("a");
  listbox.Items.Add("b");
  listbox.Items.Add("c");
} finally {
  listBox.EndUpdate(); // End-вызов
}//try

то есть код после Begin-вызова заключают в блок try, а End-вызов в finally. Это позволяет (в скобках заметим, худо-бедно) гарантировать, что в случае возникновения исключения между вызовами объект, методы которого вызываются, останется в согласованном состоянии.

Конечно, использовать try-finally каждый раз при одинаковых вызовах очень неудобно. О способе избежать такого неудобства я и расскажу.

Обычно, для упрощения вызова “парных” методов, используют класс-хелпер, реализующий IDisposable Interface, и вызывающий в своей реализации Dispose() End-метод:

internal sealed class ListBoxUpdateHelper : IDisposable
{
  private readonly ListBox listBox;
 
  public ListBoxUpdateHelper(ListBox listBox) {
    if(listBox == null) {
      throw new ArgumentNullException("listBox");
    }//if
 
    this.listBox = listBox;
    ListBox.BeginUpdate();
  }
 
  private ListBox ListBox {
    [DebuggerStepThrough]
    get { return listBox; }
  }
 
  public void Dispose() {
    ListBox.EndUpdate();
  }
}

Теперь вызывать BeginUpdate и EndUpdate удобнее:

using(new ListBoxUpdateHelper(listBox)) {
  // Некоторая полезная работа
  listbox.Items.Add("a");
  listbox.Items.Add("b");
  listbox.Items.Add("c");
}//using

Мне он не нравится по той причине, что требует необходимости завести новую сущность – тип-хелпер и всюду в месте применения её использовать. Для пользователя библиотеки это может оказаться сложным: что бы легко и удобно пользоваться некоей функциональностью типа (ListBox в нашем примере) надо “позвать на помощь” другой тип. А если различных парных методов требуется несколько? Несколько и типов-хелперов.

Поэтому прогрессивное сообщество пошло дальше, вместо типа-хелпера предоставив пользователю метод-хелпер:

internal static class ListBoxExtensions
{
  public static IDisposable DoUpdate(this ListBox listBox) {
    return new ListBoxUpdateHelper(listBox);
  }
 
  private sealed class ListBoxUpdateHelper : IDisposable
  {
    private readonly ListBox listBox;
 
    public ListBoxUpdateHelper(ListBox listBox) {
      if(listBox == null) {
        throw new ArgumentNullException("listBox");
      }//if
 
      this.listBox = listBox;
      ListBox.BeginUpdate();
    }
 
    private ListBox ListBox {
      [DebuggerStepThrough]
      get { return listBox; }
    }
 
    public void Dispose() {
      ListBox.EndUpdate();
    }
  }
}

Который можно использовать так:

using(listBox.DoUpdate()) {
  // Некоторая полезная работа
  listbox.Items.Add("a");
  listbox.Items.Add("b");
  listbox.Items.Add("c");
}//using

Теперь “снаружи” виден лишь один метод DoUpdate(), по типу возвращаемого значения которого (IDisposable) вызывающий будет знать, что метод, по возможности, лучше вызвать в блоке using.

Указанный подход можно найти в большом количестве библиотек, например в R Smart Application Toolkit.

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

public static class Disposable
{
  public static IDisposable New(Action dispose) {
    return new ActionDisposable(dispose);
  }
 
  [Serializable]
  [DebuggerDisplay("{ Action != null ? \"<Action>\" : \"<Action (Empty)>\" }")]
  private sealed class ActionDisposable : IDisposable
  {
    public ActionDisposable(Action action) {
      Action = action;
    }
 
    private Action Action { get; set; }
 
    public void Dispose() {
      if(Action != null) {
        Action();
        Action = null;
      }//if
    }
  }
}

С ним упростилось написание методов, аналогичных вышеприведённому DoUpdate():

internal static class ListBoxExtensions
{
  public static IDisposable DoUpdate(this ListBox listBox) {
    listBox.BeginUpdate();
    return Disposable.New(listBox.EndUpdate);
  }
}

Что позволяет быстрее и проще, а, значит, и чаще использовать описанный паттерн (не побоюсь этого слова :о) – а кстати, как его можно назвать?).

Подробнее о классе Disposable и ещё об одном обнаруженном с его помощью “паттерне” я раскажу как-нибудь в следующий раз.

P.S. Касательно “худо-бедно”: существует возможность того, что поток, в котором выполняется рассматриваемый нами код, будет прерван вызовом Thread.Abort(…) из другого потока сразу после вызова Begin-метода и перед тем, как начнётся try-блок. Источник: Locks and exceptions do not mix.

7 комментариев:

  1. Большое спасибо, очень интересные приёмы =)
    Особенный респект за ссылочку на "Locks and exceptions do not mix", срочно побежал переписывать код с Monitor'ами, только не понял почему в решении C#4.0 могут дедлоки возникать... =(

    ОтветитьУдалить
  2. 2Пельмешко: Переписывать надо не "код с Monitor'ами", а "код с Thread::Abort()".

    ОтветитьУдалить
  3. Fred, я вот заметил, что на форуме RSDN в разделе .NET GUI когда возникают чуть более менее сложные вопросы, то зачастую отвечаете только вы. Я к тому, что можно в блоге если что на эту тему писать :)

    ОтветитьУдалить
  4. @MozgC
    WinForms мне уже подчти совсем не интересны, а WPF я ещё только изучаю.
    Остаются только вопросы баиндинга и прочего, связанного с компонентной моделью. Тут есть идея написать про класс Attribute и его роль в System.CompoentModel, но не уверен что это то, что ты имел в виду :о)

    ОтветитьУдалить
  5. >WinForms мне уже подчти совсем не интересны

    Жаль, думаю WinForms еще долго проживет, одни программисты ленивы, другие просто медленны, третьи не будут переходить пока не появится достаточно информации и готовых решений по новой технологии. Да и компьютеры во многих компаниях просто не потянут..

    >Тут есть идея написать про класс Attribute и его роль в System.CompoentModel, но не уверен что это то, что ты имел в виду :о)

    Да, это не то что я имел в виду. Честно говоря я даже не знаю про какую-то особую роль Attribute в ComponentModel :)

    ОтветитьУдалить
  6. Кстати, по поводу WPF, заметил что на форумах на вопросы по WPF заметно меньше ответов. Имхо это очень важный фактор в переходе на WPF. Так вот возникнет какая-нибудь нетривиальная проблема и фиг решение найдешь если что.

    ОтветитьУдалить
  7. Так он же эти сложные вопросы и задаёт.

    ОтветитьУдалить