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?
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);
}