I was recently working on a 2.0 Framework project where we had many types of related objects. However, we needed to ensure that there were not multiple instances of the same object in memory. Usually the GOF Proxy pattern is used to hide or control access to an object, but we can also use it to define relationships between objects.
The structure of a family and the resulting relationships between family members roughly correlates the types of relationships we were trying to build between objects. Let's say we have a person Bill who is father to Mary and brother to Peter. Peter is an uncle to Mary, but he is also a brother to Bill. The trick is how to represent this in our code.
First, lets look at the wrong way to tackle this so you can see why we needed the Proxy pattern.
Bad Design
Here's our Person class:
class BadDesignPerson
{
private string _Name;
private int _Age;
private Collection<BadDesignRelative> _Relatives;
public BadDesignPerson(string pName, int pAge)
{
_Name = pName;
_Age = pAge;
_Relatives = new Collection<BadDesignRelative>();
}
public int Age
{
...
}
public string Name
{
...
}
internal Collection<BadDesignRelative> Relatives
{
...
}
}
And here's our BadDesignRelative class: Can you see what is wrong with the following implementation?
class BadDesignRelative:BadDesignPerson
{
public BadDesignRelative(string pName, int pAge, string pRelation)
: base(pName, pAge)
{
_Relation = pRelation;
}
private string _Relation;
public string Relation
{
get{ return _Relation;}
set{ _Relation = value; }
}
}
Even though Bill "is-a" father to Mary and we set up the relationship appropriately through inheritance, we run into a problem when Bill has a birthday and his age changes as below:
static public void Go()
{
BadDesignPerson Mary = new BadDesignPerson("Mary", 5);
BadDesignPerson Bill = new BadDesignPerson("Bill", 28);
Mary.Relatives.Add(new BadDesignRelative("Bill", 28, "Father"));
Bill.Age = 29;
}
We actually have two "Bills" in memory
Bill number 1: Bill.Age == 29
Bill number 2: Mary.Relatives[0].Age == 28
Mary's first and only relative is her father, Bill. Because Bill is not himself, this implementation is not correct and so we have to start from scratch.
The Solution.
Ok, so Bill is a Person first and foremost. Bill can also be a father, brother, uncle, nephew, or any other relation you can dream up. Obvoiusly we need to wrap Bill as the appropriate relation type depending on who's perspective we are looking at him from.
Step 1: Define the interface.
We need a IPerson interface that our wrapper class and person class will implement. (We're calling our wrapper class PersonProxy). We also need an event we can fire off so if there is a change everything on the UI will get updated.
public interface IPerson
{
int Age { get; set; }
string Name { get; set; }
Collection<PersonProxy> Relatives { get; set; }
event PersonChangeHandler Changed;
}
Step 2: Create the object class.
Both our PersonProxy and Person classes will implement IPerson. Here's the Person class:
public class Person : IPerson, ICloneable
{
#region Declarations
private int _Age;
private string _Name;
private Collection<PersonProxy> _Relatives;
#endregion
#region Constructor
public Person()
{
...
}
public Person(string pName, int pAge)
{
...
}
#endregion
#region Properties
public int Age
{
...
}
public string Name
{
...
}
public Collection<PersonProxy> Relatives
{
...
}
public event PersonChangeHandler Changed;
#endregion
#region Event Wireup
...
#endregion
}
Step 3: Create the proxy class.
Here's the PersonProxy class. It wraps the Person object, passing all the interface properties through and bubbles the event. It also has the ProxyName property which will be our way to label the relationship between People ("Father", "Brother", "Daughter", etc).
public class PersonProxy: IPerson, ICloneable
{
#region Declarations
private IPerson _Person;
private string _ProxyName;
#endregion
#region Constructor
public PersonProxy(IPerson pPerson, string pProxyName)
{
_Person = pPerson;
_Person.Changed += new PersonChangeHandler(PersonChanged);
_ProxyName = pProxyName;
}
#endregion
#region Properties
public string ProxyName
{
get { return _ProxyName; }
set { _ProxyName = value;
this.PersonChanged();
}
}
#endregion
#region IPerson Members
public int Age
{
get
{
return _Person.Age;
}
set
{
_Person.Age = value;
}
}
public string Name
{
get
{
return _Person.Name;
}
set
{
_Person.Name = value;
}
}
public Collection<PersonProxy> Relatives
{
get
{
return _Person.Relatives;
}
set
{
_Person.Relatives = value;
}
}
public event PersonChangeHandler Changed;
#endregion
#region ICloneable Members
public object Clone()
{
return _Person.Clone();
}
#endregion
#region Event Wireup
public void PersonChanged()
{
if (this.Changed != null) Changed(this);
}
public void PersonChanged(IPerson pPerson)
{
// Bubble event
PersonChanged();
}
#endregion
}
Step 4: Wire it up.
Now, when we wire things up, we will only have one and only one Bill.
IPerson mary = new Person("Mary", 10);
IPerson bill = new Person("Bill", 45);
PersonProxy father = new PersonProxy(bill, "Father");
mary.Relatives.Add(father);
PersonProxy daughter = new PersonProxy(mary, "Daughter");
father.Relatives.Add(daughter);
IPerson peter = new Person("Peter", 50);
PersonProxy brother = new PersonProxy(peter, "Brother");
father.Relatives.Add(brother);
PersonProxy uncle = new PersonProxy(peter, "Uncle");
daughter.Relatives.Add(uncle);
Download and try out the attached project to see that Bill is now himself. The Person object is updated after you edit a textbox and leave it.
Until next time,
-Happy Coding