Часто изменение объекта ведет к необходимости вызова метода для обработки новых или отредактированных данных. Например, после добавления записи необходимо вызывать метод сортировки. В этом случае, при добавлении серии получится множество ненужных вызовов. Логичным решением будет отложить сортировку до окончания изменений. Как лучше это сделать? Рассмотрим один из вариантов решения данной проблемы.
Данная реализация была написана после прочтения заметки "Postponing actions" в блоге Эмиля Джонгериуса (Emiel Jongerius). Отличия, из-за которых я написал свой вариант, указаны в конце текста.
Предлагаемая реализация отложенного действия использует конструкцию using для определения блока кода, где выполнение определенного метода может быть отложено. Вызовы этого метода внутри блока будут откладываться до момента выхода из него. Вне блока – выполняться немедленно. Кроме этого:
- реализация потокобезопасная;
- если метод ни разу не был вызван внутри блока, то при выходе из него он не будет выполнен;
- конструкции using могут быть вложенными. В этом случае метод будет вызван при выходе из самого старшего блока.
Перейдем к коду. Конструкция using требует реализации IDisposable. Для этой цели создан вспомогательный вложенный класс DisposableInterfaceHelper. При выходе из using он вызовет переданный в конструкторе метод.
/// <summary>Nested class. Perform an action when the instance is disposed.</summary>
private class DisposableInterfaceHelper : IDisposable
{
/// <summary>Postponed action helper.</summary>
private Action _onDispose;
/// <summary>Initializes a new instance of the DisposableInterfaceHelper class.</summary>
/// <param name="onDispose">The action to be called on dispose.</param>
public DisposableInterfaceHelper(Action onDispose)
{
if (onDispose == null) {
throw new ArgumentNullException("onDispose");
}
this._onDispose = onDispose;
}
/// <summary>IDisposable implementation: Execute action on dispose.</summary>
void IDisposable.Dispose()
{
this._onDispose();
}
}
Логика отложенного действия реализована в классе PostponableAction. Она учитывает входы и выходы из блоков using. Это определяет поведение при запросе на выполнение откладываемого метода. Основные методы и свойства PostponableAction:
- public PostponableAction(Action sourceAction) – конструктор. Получает в качестве параметра откладываемый метод.
- public void Execute() – запрос на выполнение отложенного метода. Вне блока using приведет к немедленному его выполнению.
- public IDisposable PostponeAction() – метод, используемый для конструкции using.
public class PostponableAction
{
#region Private fields
/// <summary>IDisposable interface helper object.</summary>
private readonly DisposableInterfaceHelper _disposableInterfaceHelper;
/// <summary>The action to be postponed.</summary>
private Action _postponeAction;
/// <summary>Postpone level counter.</summary>
private int _postponeLevel = 0;
/// <summary>Controls postponed action execution.</summary>
private bool _mustExecute = false;
/// <summary>Lock object (for thread safety).</summary>
private object _lockObject = new object();
#endregion
/// <summary>Initializes a new instance of the PostponableAction class.</summary>
/// <param name="sourceAction">The action to be postponed.</param>
public PostponableAction(Action sourceAction)
{
if (sourceAction == null) {
throw new ArgumentNullException("sourceAction");
}
this._disposableInterfaceHelper = new DisposableInterfaceHelper(this.OnDisposeHelper);
this._postponeAction = sourceAction;
}
#if DEBUG
/// <summary>Finalizes an instance of the PostponableAction class. For debug only.</summary>
~PostponableAction()
{
if (this._postponeLevel != 0) {
throw new InvalidOperationException("Postponed action planned but not executed.");
}
}
#endif
#region Properties
/// <summary>Gets a value indicating whether postponing is active.</summary>
public bool IsPostponing
{
get { return this._postponeLevel > 0; }
}
#endregion
/// <summary>Returns IDisposable interface.
/// This allow using the 'using' keyword to define where action will be postponed.</summary>
/// <returns>IDisposable interface.</returns>
public IDisposable PostponeAction()
{
lock (this._lockObject) {
this._postponeLevel++;
}
return this._disposableInterfaceHelper;
}
/// <summary>Initiates the action execution.
/// If postponing is not active then action will be executed immediately.</summary>
public void Execute()
{
lock (this._lockObject) {
if (this._postponeLevel > 0) {
this._mustExecute = true;
return;
}
}
this._postponeAction();
}
/// <summary>Executes postponed action if required.
/// This method will be called from DisposableInterfaceHelper on dispose.</summary>
private void OnDisposeHelper()
{
lock (this._lockObject) {
this._postponeLevel--;
if ((this._postponeLevel > 0) || !this._mustExecute) {
return;
}
this._mustExecute = false;
}
this._postponeAction();
}
}
Рассмотрим использование отложенного действия на примере метода Sort():
- создаем private метод SortImplementation() и переносим туда исходную реализацию;
- добавляем переменную PostponableAction _postponableSort, которая будет связана с SortImplementation();
- исходный метод Sort() теперь просто вызывает PostponableAction.Execute();
- для удобства определяем метод PostponeSort, которое будет использоваться для блоков using.
Подробности хорошо видны в исходном коде примера:
public class TestObject
{
public TestObject()
{
this._postponableSort = new PostponableAction(this.SortImplementation);
}
#region Postponable Sort
private PostponableAction _postponableSort;
private void SortImplementation()
{
Console.WriteLine("Sort > sorting ...");
}
public void Sort()
{
this._postponableSort.Execute();
}
public IDisposable PostponeSort()
{
return this._postponableSort.PostponeAction();
}
#endregion
public void Operation1()
{
// Do something ...
Console.WriteLine("Operation1");
this.Sort();
}
public void Operation2()
{
// Do something ...
Console.WriteLine("Operation2");
this.Sort();
}
public void Operation3()
{
Console.WriteLine("Operation3 > starting ... ");
using (this.PostponeSort()) {
this.Operation1();
this.Operation2();
}
Console.WriteLine("Operation3 > completed.");
}
}
В методе Operation3() реальный вызов метода сортировки произойдет только при выходе из блока using. Так же можно использовать отложенный вызов из кода вне класса:
using (testObject.PostponeSort()) {
testObject.Operation1();
testObject.Operation2();
testObject.Operation3();
}
Дополнительно в приложенном исходном коде я оставил реализацию без использования вспомогательного класса - PostponableActionUnsafe. Она компактнее, но имеет недостаток – возможность некорректного использования. Причина заключается в прямом наследовании от интерфейса IDisposable. Это позволяет вместо
public IDisposable PostponeSort { get { return this._postponableSort.PostponeAction(); } }
написать
public IDisposable PostponeSort { get { return this._postponableSort; } }
, что не вызовет ошибку на этапе компиляции. Но при этом вход в блок using не будет зафиксирован. Ситуация отслеживается только в run-time и вызывает исключение InvalidOperationException при выходе из блока.
В завершении отмечу отличия от реализации Эмиля Джонгериуса:
- Отсутствие вызова отложенного метода в финализаторе вспомогательного класса (и, как следствие, проще его код). Причина в неопределенности момента вызова финализатора.
- Используется только один экземпляр вспомогательного класса (вместо создания отдельного экземпляра для каждого блока using).
- Вариант без использования вспомогательного класса.
Исходный код (C#, Visual Studio 2010):
PostponableActionDemo.zip (14.40 kb)