Зарегистрировать обработчик событий для определенного подкласса

Хорошо, вопрос структуры кода:

Скажем, у меня есть класс FruitManager, который периодически получает Fruit объекты из некоторого источника данных. У меня также есть некоторые другие классы, которые необходимо получать, когда эти объекты Fruit будут получены. Тем не менее, каждый класс интересуется только некоторыми видами фруктов, и каждый плод имеет другую логику того, как его следует обрабатывать. Скажем, например, класс CitrusLogic имеет методы OnFruitReceived(Orange o) и OnFruitReceived(Lemon l), которые следует вызывать, когда получен соответствующий подтип плода, но его не нужно уведомлять о других плодах.

Есть ли способ элегантно справиться с этим в С# (предположительно с событиями или делегатами)? Очевидно, я мог бы просто добавить общие OnFruitReceived(Fruit f) обработчики событий и использовать инструкции if для фильтрации нежелательных подклассов, но это кажется неэлегантным. Кто-нибудь имеет лучшую идею? Спасибо!

Изменить. Я нашел общих делегатов, и похоже, что они могут быть хорошим решением. Это звучит как хорошее направление?

Ответ 1

Во-первых, Unity поддерживает подмножество .NET 3.5, где конкретное подмножество зависит от ваших параметров сборки.

Переходя к вашему вопросу, общий шаблон события в С# заключается в использовании делегатов и ключевого слова event. Поскольку вы хотите, чтобы обработчики вызывались только в том случае, если входящий фрукт совместим с его методом определения, вы можете использовать словарь для выполнения поиска. Трюк - это тип хранения делегатов. Вы можете использовать малую магию типа, чтобы заставить ее работать и хранить все как

Dictionary<Type, Action<Fruit>> handlers = new Dictionary<Type, Action<Fruit>>();

Это не идеально, потому что теперь все обработчики, похоже, принимают Fruit вместо более конкретных типов. Однако это только внутреннее представление, публично люди все равно будут добавлять определенные обработчики через

public void RegisterHandler<T>(Action<T> handler) where T : Fruit

Это позволяет публичному API очистить и ввести конкретный тип. Внутренне делегат должен измениться с Action<T> на Action<Fruit>. Для этого создайте новый делегат, который возьмет Fruit и преобразует его в T.

Action<Fruit> wrapper = fruit => handler(fruit as T);

Это, конечно, не безопасный бросок. Он сработает, если ему передано что-либо, что не является T (или наследуется от T). Вот почему очень важно, что он хранится только внутри, а не снаружи класса. Сохраните эту функцию под клавишей Type typeof(T) в словаре обработчиков.

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

Наконец, может быть открыто нормальное событие, позволяющее добавлять обработчики catch-all Fruit обычным способом.

Ниже приведен полный пример. Обратите внимание, что пример довольно минимален и исключает некоторые типичные проверки безопасности, такие как проверка нуля. Существует также потенциальный бесконечный цикл, если нет цепочки наследования от child до parent. Фактическая реализация должна быть расширена по мере возможности. Он также может использовать несколько оптимизаций. В частности, в сценариях с высоким уровнем использования кэширование цепей наследования может быть важным.

public class Fruit { }

class FruitHandlers
{
    private Dictionary<Type, Action<Fruit>> handlers = new Dictionary<Type, Action<Fruit>>();

    public event Action<Fruit> FruitAdded
    {
        add
        {
            handlers[typeof(Fruit)] += value;
        }
        remove
        {
            handlers[typeof(Fruit)] -= value;
        }
    }

    public FruitHandlers()
    {
        handlers = new Dictionary<Type, Action<Fruit>>();
        handlers.Add(typeof(Fruit), null);
    }

    static IEnumerable<Type> GetInheritanceChain(Type child, Type parent)
    {
        for (Type type = child; type != parent; type = type.BaseType)
        {
            yield return type;
        }
        yield return parent;
    }

    public void RegisterHandler<T>(Action<T> handler) where T : Fruit
    {
        Type type = typeof(T);
        Action<Fruit> wrapper = fruit => handler(fruit as T);

        if (handlers.ContainsKey(type))
        {
            handlers[type] += wrapper;
        }
        else
        {
            handlers.Add(type, wrapper);
        }
    }

    private void InvokeFruitAdded(Fruit fruit)
    {
        foreach (var type in GetInheritanceChain(fruit.GetType(), typeof(Fruit)))
        {
            if (handlers.ContainsKey(type) && handlers[type] != null)
            {
                handlers[type].Invoke(fruit);
            }
        }
    }
}

Ответ 2

Это похоже на проблему для Шаблон наблюдателя. Используя System.Reactive.Linq, мы также получаем доступ к классу Observable, который содержит ряд методов Linq для наблюдателей, включая .OfType<>

fruitSource.OfType<CitrusFruit>.Subscribe(new CitrusLogic());
fruitSource.OfType<LemonFruit>.Subscribe(new LemonLogic());

...
public class Ciruslogic : IObersver<CitrusFruit>
{ ... }

Если вам нужно добавить все существующие перегрузки по типу, например, все реализации AFruitLogic<TFruit>, вам нужно будет сканировать сборку с помощью отражения или просмотреть различные методологии IoC, такие как MEF

Ответ 3

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

Следующий код не написан в .Net2.0, но вы можете легко измените его, чтобы быть совместимым с .Net2.0, исключив использование нескольких методов Linq.

namespace Eventing
{
    public class EventAggregator : IEventAggregator
    {
        private readonly Dictionary<Type, List<WeakReference>> eventSubscriberLists =
            new Dictionary<Type, List<WeakReference>>();
        private readonly object padLock = new object();

        public void Subscribe(object subscriber)
        {
            Type type = subscriber.GetType();
            var subscriberTypes = GetSubscriberInterfaces(type)
                .ToArray();
            if (!subscriberTypes.Any())
            {
                throw new ArgumentException("subscriber doesn't implement ISubscriber<>");
            }

            lock (padLock)
            {
                var weakReference = new WeakReference(subscriber);
                foreach (var subscriberType in subscriberTypes)
                {
                    var subscribers = GetSubscribers(subscriberType);
                    subscribers.Add(weakReference);
                }
            }
        }

        public void Unsubscribe(object subscriber)
        {
            Type type = subscriber.GetType();
            var subscriberTypes = GetSubscriberInterfaces(type);

            lock (padLock)
            {
                foreach (var subscriberType in subscriberTypes)
                {
                    var subscribers = GetSubscribers(subscriberType);
                    subscribers.RemoveAll(x => x.IsAlive && object.ReferenceEquals(x.Target, subscriber));
                }
            }
        }

        public void Publish<TEvent>(TEvent eventToPublish)
        {
            var subscriberType = typeof(ISubscriber<>).MakeGenericType(typeof(TEvent));
            var subscribers = GetSubscribers(subscriberType);
            List<WeakReference> subscribersToRemove = new List<WeakReference>();

            WeakReference[] subscribersArray;
            lock (padLock)
            {
                subscribersArray = subscribers.ToArray();
            }

            foreach (var weakSubscriber in subscribersArray)
            {
                ISubscriber<TEvent> subscriber = (ISubscriber<TEvent>)weakSubscriber.Target;
                if (subscriber != null)
                {
                    subscriber.OnEvent(eventToPublish);
                }
                else
                {
                    subscribersToRemove.Add(weakSubscriber);
                }
            }
            if (subscribersToRemove.Any())
            {
                lock (padLock)
                {
                    foreach (var remove in subscribersToRemove)
                        subscribers.Remove(remove);
                }
            }
        }

        private List<WeakReference> GetSubscribers(Type subscriberType)
        {
            List<WeakReference> subscribers;
            lock (padLock)
            {
                var found = eventSubscriberLists.TryGetValue(subscriberType, out subscribers);
                if (!found)
                {
                    subscribers = new List<WeakReference>();
                    eventSubscriberLists.Add(subscriberType, subscribers);
                }
            }
            return subscribers;
        }

        private IEnumerable<Type> GetSubscriberInterfaces(Type subscriberType)
        {
            return subscriberType
                .GetInterfaces()
                .Where(i => i.IsGenericType &&
                    i.GetGenericTypeDefinition() == typeof(ISubscriber<>));
        }
    }

    public interface IEventAggregator
    {
        void Subscribe(object subscriber);
        void Unsubscribe(object subscriber);
        void Publish<TEvent>(TEvent eventToPublish);
    }

    public interface ISubscriber<in T>
    {
        void OnEvent(T e);
    }
}

Ваши модели или все, что вы хотите опубликовать

public class Fruit
{

}

class Orange : Fruit
{
}

class Apple : Fruit
{
}

class Lemon : Fruit
{
}

//Class which handles citrus events
class CitrusLogic : ISubscriber<Orange>, ISubscriber<Lemon>
{
    void ISubscriber<Orange>.OnEvent(Orange e)
    {
        Console.WriteLine(string.Format("Orange event fired: From {0}", this.GetType().Name));
    }

    void ISubscriber<Lemon>.OnEvent(Lemon e)
    {
        Console.WriteLine(string.Format("Lemon event fired: From {0}", this.GetType().Name));
    }
}

//Class which handles Apple events
class AppleLogic : ISubscriber<Apple>
{
    void ISubscriber<Apple>.OnEvent(Apple e)
    {
        Console.WriteLine(string.Format("Apple event fired: From {0}", this.GetType().Name));
    }
}

Затем используйте его следующим образом

void Main()
{
    EventAggregator aggregator = new EventAggregator();

    CitrusLogic cl =new CitrusLogic();
    AppleLogic al =new AppleLogic();
    aggregator.Subscribe(cl);
    aggregator.Subscribe(al);
    //...

    aggregator.Publish(new Apple());
    aggregator.Publish(new Lemon());
    aggregator.Publish(new Orange());
}

Какие выходы

Apple event fired: From AppleLogic
Lemon event fired: From CitrusLogic
Orange event fired: From CitrusLogic

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

Ответ 4

Я бы предложил шаблон структуры ответственности. Вы можете создать цепочку FruitHandlers. Как только плод получен, он проходит через эту цепочку, пока обработчик не сможет обработать свой тип плода.

Ответ 5

Во-первых, не используйте инструкции if для маршрутизации вашей логики. Если вы закончите использовать общий обработчик, передайте все фрукты всем обработчикам и дайте фильтрам обработать фильтр. Это сэкономит вам боль в обслуживании в долгосрочной перспективе.

Что касается вопроса о том, что было бы самым эффективным средством маршрутизации плода через обработчиков, это более сложный вопрос, поскольку он сильно зависит от вашей конкретной ситуации.

Я бы сделал, чтобы создать фасад обработки фруктов, который принимает все классы XLogic и имеет некоторый тип метода регистрации, например

IFruitHandlers fruitHandlers;
fruitHandlers.Register(new CitrusLogic()) // Or some good DI way of doing this

// later
fruitHandlers.Handle(fruit);

Затем, внутренне, вы можете иметь дело с различными реализациями, чтобы увидеть, что работает. Например, для определения логического обработчика, например:

public class FruitLogic<T> where T:Fruit {}

Вы можете создать внутреннюю таблицу поиска в реализации обработчика фруктов

Dictionary<Type, List<IFruitLogic>> fruitHandlers;

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

В вашем случае по умолчанию вы также можете просто

List<FruitLogic> handlers;

и каждый обработчик позаботится о собственной фильтрации.

Важно установить API, который позволяет гибко играть с деталями реализации для того, что является оптимальным для вашего домена. Измерение производительности различных решений в реалистичной среде - единственный способ найти наилучшее решение для вас.

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

Ответ 6

Очевидно, я мог бы просто добавить общие обработчики событий OnFruitReceived (Fruit f) и использовать инструкции if для фильтрации нежелательных подклассов

Я боюсь, что вы не найдете другого пути, или на самом деле вы не найдете более короткого пути, поэтому я предлагаю сэкономить ваше время и начать вводить ваши инструкции if.