Простая система событий — нестандартный подход

в 11:25, , рубрики: .net, Events, unity3d, ненормальное программирование

Данная заметка является логическим продолжением поста «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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js