Данная заметка является логическим продолжением поста «SMessage — Простая и предсказуемая система событий для Unity». Пользователь erlioniel описал два подхода к построению собственной системы сообщений: с использованием перечислений и классов-сообщений. В своей заметке я хочу рассказать о возможном пути смешения этих подходов и о велосипеде, который может из этого получиться.
Скрещиваем ужа с ежом — третий подход
Очевидно, что статическая типизация позволяет отсеять большое кол-во ошибок и помогает в написании кода. Но реализация через enum'ы имеет свое преимущество — вместо создания нового объекта сообщения происходит прямая передача параметров в функцию-обработчик.
Можно ли избавиться от инстанцирования объектов-сообщений, но не потерять всех плюсов статической типизации?
Да, можно. Для этого сформулируем подробнее, что мы хотим получить. Нам нужен результат аналогичный методу отправки сообщения через enum. То есть метод вида:
events.Dispatch( идентификатор события, параметры события );
При этом «идентификатор события» должен однозначно определять «параметры события».
На помощь нам придут generic'и. Первая сигнатура, которая приходит в голову, выглядит следующим образом:
public void Dispatch<TEvent, TSender, TArg>(TEvent e, TSender sender, TArg arg)
Очевидно, что она ничем не лучше реализации через enum'ы, так как TEvent
, TSender
и TArg
независимы. Но мы знаем, что событие должно однозначно определять свои параметры. Опишем это следующим образом:
public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> e, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg>
Во-первых, обратим внимание на то, что первый аргумент описывает собой все generic-типы метода. Во-вторых, у нас появилась однозначная зависимость между событием и его параметрами. Теперь IDE сможет по первому аргументу определить типы остальных параметров.
А что такое IEvent
? Рассмотрим подробнее ограничения параметров типа (конструкцию where
). Видно, что тип TEvent
рекурсивно определен через самого себя. Поэтому и IEvent
определим также:
public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg> { }
Отлично, теперь мы можем объявить свое событие вместе с типами параметров:
public class StartTurnEvent : IEvent<StartTurnEvent, Player, int> { }
Вернемся к вопросу отправки сообщения. Сейчас она выглядит следующим образом:
events.Dispatch(new StartTurnEvent(), player, 123);
Мы пришли к синтаксису схожему с enum'ами, но инстанцирование никуда не делось.
С другой стороны, очевидно, что нас интересует не сам объект, а его тип. Удостоверимся в этом, заглянув внутрь метода Dispatch
:
public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> e, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg>
{
foreach (var action in handlers.Get<Action<TSender, TArg>>(typeof(TEvent)))
{
action(sender, arg);
}
}
Видно, что первый аргумент не используется вообще, то есть можно вызывать Dispatch
без инстанцирования:
events.Dispatch((StartTurnEvent)null, player, 123);
Последним штрихом избавимся от лишних скобок и слова null
. Вынесем его в константу:
public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg>
{
public readonly static TEvent Tag = null;
}
Что же мы получили в итоге?
1. Синтаксис схожий с отправкой сообщений, через enum'ы.
2. Статическую типизацию и подсказки IDE.
3. Избавились от инстанцирования сообщений перед каждой отправкой.
Сравнение подходов
Объявление событий:
// Способ 0: стандартная система сообщений Unity3D
/* Объявление событий не требуется */
// Способ 1: отправка через enum'ы
enum Events { StartTurn }
// Способ 2: типизированная система сообщений
public class StartTurnMessage : IMessage<Player, int>
{
public StartTurnMessage(Player player, int value) : base(player, value) { }
}
// Способ 3: смешанный
public class StartTurnEvent : IEvent<StartTurnEvent, Player, int> { }
Отправка сообщений:
// Способ 0: стандартная система сообщений Unity3D
player.SendMessage("OnStartTurn", 123); // Никаких подсказок от IDE
// Способ 1: отправка через enum'ы
events.Dispatch(Events.StartTurn, player, 123); // Только подсказки имени события
// Способ 2: типизированная система сообщений
events.Dispatch(new StartTurnMessage(player, 123));
// Способ 3: смешанный
events.Dispatch(StartTurnEvent.Tag, player, 123);
using System;
using System.Collections.Generic;
using System.Linq;
using Collection = System.Collections.Generic.HashSet<object>;
namespace SteamSquad.Gameplay
{
public class EventBus
{
private readonly Container handlers = new Container();
public void Unsubscribe<TEvent, TSender>(IEvent<TEvent, TSender> tag, Action<TSender> action) where TEvent : IEvent<TEvent, TSender>
{
handlers.Remove(typeof(TEvent), action);
}
public void Unsubscribe<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, Action<TSender, TArg> action) where TEvent : IEvent<TEvent, TSender, TArg>
{
handlers.Remove(typeof(TEvent), action);
}
public void Subscribe<TEvent, TSender>(IEvent<TEvent, TSender> tag, Action<TSender> action) where TEvent : IEvent<TEvent, TSender>
{
handlers.Add(typeof(TEvent), action);
}
public void Subscribe<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, Action<TSender, TArg> action) where TEvent : IEvent<TEvent, TSender, TArg>
{
handlers.Add(typeof(TEvent), action);
}
public void Dispatch<TEvent, TSender>(IEvent<TEvent, TSender> tag, TSender sender) where TEvent : IEvent<TEvent, TSender>
{
foreach (var action in handlers.Get<Action<TSender>>(typeof(TEvent)))
{
action(sender);
}
}
public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg>
{
foreach (var action in handlers.Get<Action<TSender, TArg>>(typeof(TEvent)))
{
action(sender, arg);
}
}
public abstract class IEvent<TEvent, TSender> where TEvent : IEvent<TEvent, TSender>
{
public readonly static TEvent Tag = null;
}
public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg>
{
public readonly static TEvent Tag = null;
}
private class Container
{
private readonly Dictionary<Type, Collection> container = new Dictionary<Type, Collection>();
public void Add(Type type, object action)
{
Collection collection;
if (!container.TryGetValue(type, out collection))
{
container.Add(type, collection = new Collection());
}
collection.Add(action);
}
public void Remove(Type type, object action)
{
container[type].Remove(action);
}
public IEnumerable<TAction> Get<TAction>(Type type)
{
Collection collection;
if (container.TryGetValue(type, out collection))
{
return collection.OfType<TAction>();
}
return Enumerable.Empty<TAction>();
}
}
}
}
Автор: AndreyMI