7
\$\begingroup\$

I wrote a MessageBus class whose only purpose is to deliver messages from object to object without direct link between them.

I encountered a problem with duplicate code caused by requirement for same methods with and without generic types, however it does bother me and a better solution would be appreciated.

using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Class to deliver messages from one object to another without directly linking them.
/// It pushes messages to channels on which objects can subscribe.
/// </summary>
public static class MessageBus
{
    /// <summary>
    /// Dictionary of message listeners
    /// </summary>
    private static Dictionary<ChannelKey, object> Listeners { get; } = new Dictionary<ChannelKey, object>();

    /// <summary>
    /// Send message to the bus
    /// </summary>
    /// <param name="key">Channel key</param>
    /// <param name="arguments">Arguments of message</param>
    public static void Send<T>(object key, T arguments)
    {
        var fullKey = new ChannelKey(key, typeof(T));
        if (!Listeners.ContainsKey(fullKey))
        {
            return;
        }

        var actionList = (List<MessageReaction<T>>)Listeners[fullKey];
        foreach (var listener in actionList)
        {
            listener.Reaction(arguments);
        }
    }

    /// <summary>
    /// Send message to the bus
    /// </summary>
    /// <param name="key">Channel key</param>
    public static void Send(object key)
    {
        var fullKey = new ChannelKey(key);
        if (!Listeners.ContainsKey(fullKey))
        {
            return;
        }

        var actionList = (List<MessageReaction>)Listeners[fullKey];
        foreach (var listener in actionList)
        {
            listener.Reaction();
        }
    }

    /// <summary>
    /// Subscribe on messages from channel with given key and given argument type
    /// </summary>
    /// <param name="subscriber">Subscriber object</param>
    /// <param name="key">Channel key</param>
    /// <param name="listenerAction">Action to react on message</param>
    public static void Subscribe<T>(this object subscriber, object key, Action<T> listenerAction)
    {
        var fullKey = new ChannelKey(key, typeof(T));
        if (!Listeners.ContainsKey(fullKey))
        {
            Listeners[fullKey] = new List<MessageReaction<T>>();
        }

        var list = (List<MessageReaction<T>>)Listeners[fullKey];
        if (list.Any(reaction => reaction.Subscriber.Equals(subscriber)))
        {
            return;
        }

        list.Add(new MessageReaction<T>(subscriber, listenerAction));
    }

    /// <summary>
    /// Subscribe on messages from channel with given key
    /// </summary>
    /// <param name="subscriber">Subscriber object</param>
    /// <param name="key">Channel key</param>
    /// <param name="listenerAction">Action to react on message</param>
    public static void Subscribe(this object subscriber, object key, Action listenerAction)
    {
        var fullKey = new ChannelKey(key);
        if (!Listeners.ContainsKey(fullKey))
        {
            Listeners[fullKey] = new List<MessageReaction>();
        }

        var list = (List<MessageReaction>)Listeners[fullKey];
        if (list.Any(reaction => reaction.Subscriber.Equals(subscriber)))
        {
            return;
        }

        list.Add(new MessageReaction(subscriber, listenerAction));
    }

    /// <summary>
    /// Unsubscribe object from channel by key and argument type
    /// </summary>
    /// <param name="subscriber">Object to unsubscribe</param>
    /// <param name="key">Channel key</param>
    public static void UnSubscribe<T>(this object subscriber, object key)
    {
        var fullKey = new ChannelKey(key, typeof(T));
        if (!Listeners.ContainsKey(fullKey))
        {
            return;
        }

        ((List<MessageReaction<T>>)Listeners[fullKey]).RemoveAll(listener => listener.Subscriber.Equals(subscriber));
    }

    /// <summary>
    /// Unsubscribe object from channel by key
    /// </summary>
    /// <param name="subscriber">Object to unsubscribe</param>
    /// <param name="key">Channel key</param>
    public static void UnSubscribe(this object subscriber, object key)
    {
        var fullKey = new ChannelKey(key);
        if (!Listeners.ContainsKey(fullKey))
        {
            return;
        }

        ((List<MessageReaction>)Listeners[fullKey]).RemoveAll(listener => listener.Subscriber.Equals(subscriber));
    }

    /// <summary>
    /// Class to represent a complex channel key
    /// </summary>
    private class ChannelKey
    {
        /// <summary>
        /// Channel argument type
        /// </summary>
        private Type ArgumentType { get; }

        /// <summary>
        /// Channel key
        /// </summary>
        private object Key { get; }

        public ChannelKey(object key, Type argumentType = null)
        {
            Key = key;
            ArgumentType = argumentType;
        }

        public override bool Equals(object @object)
        {
            var anotherKey = @object as ChannelKey;
            return anotherKey != null && anotherKey.Key.Equals(Key) && anotherKey.ArgumentType == ArgumentType;
        }

        public override int GetHashCode()
        {
            return (Key?.GetHashCode() ?? 0) + (ArgumentType?.GetHashCode() ?? 0);
        }
    }

    /// <summary>
    /// Class to represent reaction on message
    /// </summary>
    /// <typeparam name="T">Type of argument</typeparam>
    private class MessageReaction<T>
    {
        /// <summary>
        /// Method to react on incoming message
        /// </summary>
        public Action<T> Reaction { get; }

        /// <summary>
        /// Object which reacts
        /// </summary>
        public object Subscriber { get; }

        public MessageReaction(object subscriber, Action<T> reaction)
        {
            Subscriber = subscriber;
            Reaction = reaction;
        }
    }

    /// <summary>
    /// Class to represent reaction on message
    /// </summary>
    private class MessageReaction
    {
        /// <summary>
        /// Method to react on incoming message
        /// </summary>
        public Action Reaction { get; }

        /// <summary>
        /// Object which reacts
        /// </summary>
        public object Subscriber { get; }

        public MessageReaction(object subscriber, Action reaction)
        {
            Subscriber = subscriber;
            Reaction = reaction;
        }
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ Do you have a good reason to support weakly typed operations? I can only think of a handful of reasons to support it and most of them involve COM Interoperability. \$\endgroup\$
    – RubberDuck
    Commented Feb 7, 2016 at 17:29
  • \$\begingroup\$ @RubberDuck, cannot quite get what you are talking about, sorry. \$\endgroup\$ Commented Feb 7, 2016 at 17:57
  • \$\begingroup\$ Why do you the non-generic versions of all these methods? \$\endgroup\$
    – RubberDuck
    Commented Feb 7, 2016 at 17:59
  • 1
    \$\begingroup\$ Well, I have them for situations like connection problems somewhere in program. So I can just send a message "Problem" and react with showing loading icon + "connection issues" text for user. That requires no arguments and that is a reason behind having non generic methods. \$\endgroup\$ Commented Feb 7, 2016 at 18:04
  • \$\begingroup\$ When it comes to overriding GetHashCode, you should have a reasonable guarantee of no collisions. I think yours could use a little help in that respect, and with that I give you Jon Skeet: stackoverflow.com/a/263416/3312 \$\endgroup\$ Commented Feb 12, 2016 at 0:48

1 Answer 1

7
\$\begingroup\$

Using ContainsKey() together with the getter of the Item property of a Dictionary<TKey, TValue> should be replaced by a call to TryGetValue() which is faster because the check if the key exists is done by the Item getter too.

Internally these three methods are calling the FindEntry() method to check whether a given key exists. So calling this method only once through the TryGetValue() method should be the way to go for instance like so

public static void Send<T>(object key, T arguments)
{

    var fullKey = new ChannelKey(key, typeof(T));
    object value;
    if (!Listeners.TryGetValue(fullKey, out value))
    {
        return;
    }

    var actionList = (List<MessageReaction<T>>)value;
    foreach (var listener in actionList)
    {
        listener.Reaction(arguments);
    }

Like you can see I have added braces for the if statement as well. Although they might be optional you should add them always to make your code less error prone.


If you are bothered by duplicated code due to the usage of the generic/non generic version you should maybe think about removing the non generic version. Without seeing the calling code and all the possibilities how the non generic version and the generic version is used its hard to tell if removing the code duplication is possible.


In general your code looks good. You are using well named methods (except for Reaction IMO) and the documentation makes it easy to understand what the code is doing.

I wouldn't have the Listeners as a private property but that's a matter of taste. I personally would prefer just a private static readonly field.

I don't quite see why the MessageReaction should know to which Subscriber it belongs. If the MessageReaction would want to talk to its "parent" it should use events so there is no need to couple this objects that much.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your answer! MessageReaction knows about Subscriber for UnSubscibe method to be able to unsubscribe this object from a channel. Without Subscriber property it is impossible, at least I think so. This is part of code which uses this property: ((List<MessageReaction<T>>)Listeners[fullKey]).RemoveAll(listener => listener.Subscriber.Equals(subscriber)) \$\endgroup\$ Commented Feb 8, 2016 at 7:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.