Pull to refresh

Паттерн «VIP слушатель»

Designing and refactoring *C# *
Признаюсь честно, описание этого паттерна мне не встречалось, соответственно его название я выдумал. Если у кого есть информация о правильном названии, буду очень рад услышать. Паттерн не привязан к языку но в данной статье я буду использовать C#.

Картинка для привлечения внимания:




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

Объекты порождаемые и уничтожаемые системой:
	public interface IObject
	{
	}


Сервис, предоставляющий доступ к объектам:
	public delegate void ServiceChangedHandle(IService sender, IObject item, bool injected);

	public interface IService
	{
		IEnumerable<IObject> Items { get; }

		event ServiceChangedHandle OnServiceChanged;
	}


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

Типичный пример слушателя:
	public class Listener
	{
		public void Initialise()
		{
			foreach (var item in service.Items)
				RegisterItem(item);

			service.OnServiceChanged += OnServiceChanged;
		}

		public void Shutdown()
		{
			service.OnServiceChanged -= OnServiceChanged;

			foreach (var item in service.Items)
				UnregisterItem(item);
		}

		private void OnServiceChanged(IService sender, IObject item, bool injected)
		{
			if (injected)
				RegisterItem(item);
			else
				UnregisterItem(item);
		}

		private void RegisterItem(IObject item)
		{
			...
		}

		private void UnregisterItem(IObject item)
		{
			...
		}

		private IService service;
	}


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

Сервис с поддержкой многопоточности:
	public interface IService
	{
		...

		// Объект для синхронизации
		object SyncRoot { get; }
	}


Слушатель с поддержкой многопоточного сервиса (Внутренняя синхронизация опущенна):
	public class Listener
	{
		public void Initialise()
		{
			// Добавляем синхронизацию
			lock (service.SyncRoot)
			{
				foreach (var item in service.Items)
					RegisterItem(item);

				service.OnServiceChanged += OnServiceChanged;
			}
		}

		public void Shutdown()
		{
			// Добавляем синхронизацию
			lock (service.SyncRoot)
			{
				service.OnServiceChanged -= OnServiceChanged;

				foreach (var item in service.Items)
					UnregisterItem(item);
			}
		}

		...
	}


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

Подписчик для многопоточного и однопоточного варианта сервиса (Внутренняя синхронизация опущена):
	public class Listener
	{
		public void Initialise()
		{
			service.OnServiceChanged += OnServiceChanged;
		}

		public void Shutdown()
		{
			service.OnServiceChanged -= OnServiceChanged;
		}

		...
	}


Реализации сервиса для однопоточного варианта:
	public class Service : IService
	{
		...

		public event ServiceChangedHandle OnServiceChanged
		{
			add
			{
				// Эмулируем добавление объектов для подписчика
				foreach (var item in items)
					value(this, item, true);

				// Непосредственная подписка
				eventHandle += value;
			}
			remove
			{
				// Непосредственная отписка
				eventHandle -= value;

				// Эмулируем исчезновение объектов
				foreach (var item in items)
					value(this, item, false);
			}
		}

		private ServiceChangedHandle eventHandle;
		private List<IObject> items = new List<IObject>();
	}


Как и у любого паттерна у этого варианта слушателя есть свои плюсы, минусы и область применения.

Плюсы:
  • Подписчики упрощаются, достаточно простой подписки и отписки
  • Одинаковый код как для многопоточного так и для однопоточного варианта


Минусы:
  • При использовании нужно знать эту особенность у сервиса, чтобы объекты не обрабатывать два раза


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


Update1
Некоторые пояснения:
В шарпе уже реализован паттерн “Наблюдатель”, вместо наблюдателя и наблюдаемого мы имеем событие и подписчиков.
VIP не синоним слова “один” а синоним слова “особый”. В данном случае все подписчики являются особыми, потому что наблюдаемый объект для каждого отдельного наблюдателя ведет себя особо. А именно, генерит события, которые наблюдатель мог пропустить или не дождаться.

Всем спасибо за внимание!
Tags:
Hubs:
Total votes 41: ↑23 and ↓18 +5
Views 17K
Comments Comments 22