Properties with events: another attempt
See also: Properties as objects
The goal here is to have a class with a property that implements the Observer pattern, based on a statement in Object Thinking that all classes should support this. (I’m not sure I agree, but I’m exploring it anyway.)
So far I’ve proposed two options:
Let the class support its own notifications of events. Downsides are:
- Duplicated code as soon as another property wants the same feature.
- 11 lines of text in my class to support the basic notion of an object with an attribute
- Possibility of a bug where the class directly modifies the backing store, without firing the OnSet event.
Create a class to do it all for you. Downsides are:
- Can’t limit access to the setter to just the containing class.
- Violates encapsulation rule of Law of Demeter by exposing an implementation detail (that I’m using Property<>).
Upon further discussion with members of the team, we’ve come up with something I like quite a bit. It balances responsibilities between the class you’re writing and the Property<> class. It seems to solve all the problems listed above.
Here’s the code:
interface IReadonlyProperty<T>
{
T Value { get; }
event Property<T>.SetEventHandler OnSet;
}
class Property<T> : IReadonlyProperty<T>
{
T _value;
public Property()
{
// put something into the event so that I don't have to check
// for null later.
this.OnSet += delegate { };
}
public delegate void SetEventHandler(object sender, T newValue);
public event SetEventHandler OnSet;
public T Value
{
get
{
return this._value;
}
set
{
this._value = value;
this.OnSet(this, this._value);
}
}
public void SetValueWithoutFiringEvent(T t) { this._value = t; }
T IReadonlyProperty<T>.Value { get { return this._value; } }
}
To demonstrate its worth, here’s the usage:
class Customer
{
Property<int> _age = new Property<int>();
public IReadonlyProperty<int> Age
{
get { return this._age; }
}
public void Test()
{
int eventFiredCount = 0;
this._age.OnSet += delegate { eventFiredCount++; };
Debug.Assert(eventFiredCount == 0);
this._age.Value = 9;
Debug.Assert(eventFiredCount == 1);
this._age.SetValueWithoutFiringEvent(7);
Debug.Assert(eventFiredCount == 1);
}
}
Note that I can still set the value internally to Customer without firing the event, but only by explicitly saying that’s what I’m doing. I could decide never to allow that by just removing the SetValueWithoutFiringEvent method.