Search code examples
c#genericsobservers

C# type specific observers


In this application, observers handle state changes in a network of objects. All objects are derived classes based on the same BaseObject class. BaseObject offers essential identification and navigation features.

Classes derived from BaseObject are created by a code generator. These classes shall have a minimum footprint and concentrate on specific state and behaviour.

At application level, observers handle state changes in BaseObject-derived classes, quite often for more than one class and mostly for a larger number of BaseObject-derived instances.

In the current solution, BaseObject manages the observers and notifies the observer with the BaseObject instance as sender.

using System;
using System.Collections.Generic;

namespace Observer {
  #region underlying framework foundation
  interface IObserver {
    void ObjectChanged (BaseObject obj);
  }

  abstract class BaseObject {
    HashSet<IObserver> observers = new HashSet<IObserver> ();

    public void RegisterObserver (IObserver observer) {
      observers.Add (observer);
    }

    public void FireObjectChanged () {
      foreach (var obs in observers)
        obs.ObjectChanged (this);
    }
  }
  #endregion underlying framework foundation

  #region code generator
  class DerivedObject1 : BaseObject {
  }

  class DerivedObject2 : BaseObject {
  }
  #endregion code generator

  #region application code
  class Observer : IObserver {

    public void ObjectChanged (BaseObject obj) {
      Console.WriteLine (obj.GetType ().Name);

      if (obj is DerivedObject1) {

      } else if (obj is DerivedObject2) {

      }
    }
  }
  #endregion application code

  #region sample
  class Program {
    static void Main (string[] args) {
      Observer observer = new Observer ();
      List<BaseObject> objects = new List<BaseObject> ();

      DerivedObject1 obj1 = new DerivedObject1 ();
      objects.Add (obj1);
      obj1.RegisterObserver (observer);

      DerivedObject2 obj2 = new DerivedObject2 ();
      objects.Add (obj2);
      obj2.RegisterObserver (observer);

      foreach (var bo in objects)
        bo.FireObjectChanged ();
    }
  }
  #endregion sample
}

What I don't like about that approach is that the observer must identify the sender type at run-time. Instead I want to make this to be safe at compile-time.

So I propose a new approach which introduces generics and a second BaseObject layer, BaseObjectT which holds type-safe observers.

using System;
using System.Collections.Generic;

namespace Observer {
    #region underlying framework foundation
    interface IObserver<T> where T : BaseObjectT<T> {
        void ObjectChanged (T obj);
    }

    abstract class BaseObject {
        public abstract void FireObjectChanged ();
    }

    abstract class BaseObjectT<T> : BaseObject where T : BaseObjectT<T> {
        HashSet<IObserver<T>> observers = new HashSet<IObserver<T>> ();

        public void RegisterObserver (IObserver<T> observer) {
            observers.Add (observer);
        }

        public override void FireObjectChanged () {
            foreach (var obs in observers)
                obs.ObjectChanged ((T)this);
        }
    }
    #endregion underlying framework foundation

    #region code generator
    class DerivedObject1 : BaseObjectT<DerivedObject1> {
    }

    class DerivedObject2 : BaseObjectT<DerivedObject2> {
    }
    #endregion code generator

    #region application code
    class Observer :
      IObserver<DerivedObject1>,
      IObserver<DerivedObject2> {

        public void ObjectChanged (DerivedObject1 obj) {
            Console.WriteLine (obj.GetType ().Name);
        }

        public void ObjectChanged (DerivedObject2 obj) {
            Console.WriteLine (obj.GetType ().Name);
        }
    }
    #endregion application code

    #region sample
    class Program {
        static void Main (string[] args) {
            Observer observer = new Observer ();
            List<BaseObject> objects = new List<BaseObject> ();

            DerivedObject1 obj1 = new DerivedObject1 ();
            objects.Add (obj1);
            obj1.RegisterObserver (observer);

            DerivedObject2 obj2 = new DerivedObject2 ();
            objects.Add (obj2);
            obj2.RegisterObserver (observer);

            foreach (var bo in objects)
                bo.FireObjectChanged ();
        }
    }
    #endregion sample
}

While this approach works and exactly does what I want at the application level - passing an instance of the derived class to the observer without the need for a type cast there, and allowing overloading of the different IObserver method implementations - it seems a bit ugly to me at the bottom layer.

My question now, is there a better, a more elegant way to accomplish this, in particular, is there a way to avoid the cast in FireObjectChanged()

obs.ObjectChanged ((T)this);      

or to combine BaseObject and BaseObjectT into a single base class?


Solution

  • As @Falanwe commented, IObserver<T> is a system class and you should call your observer something else. I chose ICustomObserver<T> for the sample code below.

    The only alternative I came up with was this. We create an extension method to store delegates in a ConditionalWeakTable. These delegates (Action<BaseObject>) can call ObjectChanged from within another extension method on a per-object basis.

    The advantage to this approach is that you don't need anything fancy in your BaseObject and that you can call RegisterObserver in a type-safe way for BaseObject or for DerivedObject1.

    The disadvantage is that there could be a learning curve if you don't already have an understanding of extension methods, delegates or ConditionalWeakTable.

    public static class ObjectChangedExtension
    {
        internal static ConditionalWeakTable<object, List<Action<BaseObject>>> observers
            = new ConditionalWeakTable<object, List<Action<BaseObject>>>();
    
        public static void RegisterObserver<T>(this T obj, ICustomObserver<T> observer)
            where T : BaseObject
        {
            Action<BaseObject> objChangedDelegate = v => observer.ObjectChanged((T)v);
    
            observers
                .GetOrCreateValue(obj)
                .Add(objChangedDelegate);
        }
    
        public static void FireObjectChanged(this BaseObject obj)
        {
            observers
                .GetOrCreateValue(obj)
                .ForEach(v => v(obj));
        }
    }
    
    #region code generator
    class DerivedObject1 : BaseObject
    {
    }
    
    class DerivedObject2 : BaseObject
    {
    }
    #endregion code generator
    
    #region application code
    class Observer : ICustomObserver<DerivedObject1>, ICustomObserver<DerivedObject2>
    {
        public void ObjectChanged(DerivedObject1 obj)
        {
            Console.WriteLine("DerivedObject1 Observer");
        }
    
        public void ObjectChanged(DerivedObject2 obj)
        {
            Console.WriteLine("DerivedObject2 Observer");
        }
    }
    
    class ObserverOfBase : ICustomObserver<BaseObject>
    {
        public void ObjectChanged(BaseObject obj)
        {
            Console.WriteLine("BaseObject Observer");
        }
    }
    #endregion application code
    
    #region sample
    class Program
    {
        internal static void Main(string[] args)
        {
            Observer observer = new Observer();
            List<BaseObject> objects = new List<BaseObject>();
    
            DerivedObject1 obj1 = new DerivedObject1();
            objects.Add(obj1);
            obj1.RegisterObserver(observer);
    
            DerivedObject2 obj2 = new DerivedObject2();
            objects.Add(obj2);
            obj2.RegisterObserver(observer);
    
            var baseObjectObserver = new ObserverOfBase();
            obj1.RegisterObserver(baseObjectObserver);
            obj2.RegisterObserver(baseObjectObserver);
    
            foreach (var bo in objects)
                bo.FireObjectChanged();
        }
    }
    #endregion sample
    
    public class BaseObject
    {
    }
    
    public interface ICustomObserver<T>
    {
        void ObjectChanged(T obj);
    }