Introduction
While playing with .NET 2.0's Generics, an interesting pattern came to my mind as a solution for a code reuse problem. I quickly implemented the solution, fairly skeptical about the C# compiler being able to compile my code successfully. To my surprise, the compiler didn't complain a bit. Still thinking that even though it compiled successfully, some weird exception might be thrown at runtime, I tried executing my piece of code. It worked flawlessly.
Background
The problem I had to solve:
The project I was working on contained several "data holder"-type classes that were similar in their purpose. These classes were all used to store different data. These data were associated with a date and had to be transmitted to another layer of the application as a list. This layer that was to receive the lists of the objects had two requirements: the data had to be sorted chronologically (by the date) and there could be no two pieces of data in a list having the same date.
So I have my classes, all having different properties to store the useful data, plus the Date
property. Then I have my List<>
's of instances of these classes. These must be sorted of course and the entries having the same dates must be eliminated. The eliminated entries don't just disappear, since we don't want to lose the data they're holding. Therefore we must combine the data of the removed entry with the data of the entry that stays in the list.
OK, so now I had a static function that would take as input parameter a strongly-typed generic list of a particular class and return a list usable for my other layer. Since I wanted everything to be strongly-typed, I had to copy-paste-edit this function for every class. Not nice, so enter Generics.
The classic solution:
So how would a regular developer solve this? Create an interface, make the data holder classes implement the interface. Create a generic class having a parameter that inherits from the interface, have the sort / eliminate dates code work with the generic parameter. Something like:
public interface IDateObj
{
DateTime Date
{
get;
set;
}
void Combine(IDateObj obj);
}
And the generic class for that:
public static class ObjListManager<T> where T : ICloneable, IDateObj
{
public static List<T> EliminateSameDates(List<T> listOrig)
{
if (listOrig.Count < 2)
return listOrig;
List<T> list = new List<T>(listOrig.Count);
foreach (T obj in listOrig)
{
T objCopy = (T)obj.Clone();
list.Add(objCopy);
}
list.Sort(delegate(T obj1, T obj2) { return obj1.Date.CompareTo(obj2.Date); });
for (int i = list.Count - 1; i >= 1; i--)
{
T curr = list[i];
T prev = list[i - 1];
if (prev.Date.Date == curr.Date.Date)
{
prev.Combine(curr);
prev.Date = curr.Date;
list.RemoveAt(i);
}
}
return list;
}
}
Not bad. This way we could do the job like this:
public class VersementObject : ICloneable, IDateObj
{
public void Combine(IDateObj obj)
{
VersementObject myObj = (VersementObject)obj;
}
}
void Main()
{
List<VersementObject> list;
list = ObjListManager<VersementObject>.EliminateSameDates(list);
}
No big deal so far. Nothing interesting, really. Thing is that I wanted to do better than that because:
- In
VersementObject.Combine
I have a parameter of type IDateObj
. But what I really want is a VersementObject
, hence I need a conversion. I want to get rid of that. - The
VersementObject
.Combine
function must be public
because it is an implementation of the IDateObj
interface. But this function is only used as a trait for the VersementObject
class in the EliminateSameDates
function, I don't need it to be visible for all users of the VersementObject
class. - I don't like long code lines, they're harder to follow when they don't fit in the editing window. With Generics it is inevitable to have long code lines (unless you name your classes using three-lettered acronyms). But still, I'd like to make the line containing the invocation of
EliminateSameDates
as short as possible. - What if I need to do additional processing after the objects having the same date are eliminated? All I can do is write a new function that calls
EliminateSameDates
and then does the processing on the list. Flexible, but not so nice. I don't want to write wrappers in the Main
code. I could add a method to the interface and have EliminateSameDates
call it, as a last step before returning. This would force all implementers of IDateObj
to provide an empty stub for this method. And a public
one, too! A lot of useless code polluting my nice classes.
About the code
So if I want to solve the 4 problems I still had, the interface has to go.
Now, the interface refuses to go away quietly because I need the Date
property in the EliminateSameDates
function. I also need Combine
. So what's the next best thing after interfaces? Abstract classes. Actually, they're better than interfaces since you can put some code in them as well, not only stubs. Of course, the big drawback is that you can only inherit from one such class at a time because C# doesn't allow multiple inheritance (note: C++ rules! ;) ). But this time, an abstract class should do perfectly well.
So, for this to work I need a generic abstract class that would only accept as a generic parameter a type derived from itself. Nice. In theory. I was thinking that the only problem with this will be that the compiler won't see the beauty of my solution. Why? Well, let's see. The fact that I use the class itself to specify constraints about the generic parameter of the very class I was using in the definition. It looked like recursion, the nasty kind that never stops.
I was wrong. The compiler was smarter than I expected. So here's the solution:
public abstract class ObjListManager<T> : ICloneable where T : ObjListManager<T>
{
public abstract DateTime Date
{
get;
set;
}
public abstract object Clone();
protected abstract void Combine(T obj);
delegate bool EliminateCriterion(T prev, T curr);
static List<T> Eliminate(List<T> listOrig, EliminateCriterion crit)
{
if (listOrig.Count < 2)
return listOrig;
List<T> list = new List<T>(listOrig.Count);
foreach (T obj in listOrig)
{
T objCopy = (T)obj.Clone();
list.Add(objCopy);
}
list.Sort(delegate(T obj1, T obj2) { return obj1.Date.CompareTo(obj2.Date); });
for (int i = list.Count - 1; i >= 1; i--)
{
T curr = list[i];
T prev = list[i - 1];
if (crit(prev, curr))
{
prev.Combine(curr);
prev.Date = curr.Date;
list.RemoveAt(i);
}
}
return list;
}
public static List<T> EliminateSameDates(List<T> listOrig)
{
return Eliminate(listOrig, delegate(T prev, T curr)
{ return prev.Date.Date == curr.Date.Date; });
}
public static List<T> EliminateSameMonths(List<T> listOrig)
{
return Eliminate(listOrig, delegate(T prev, T curr)
{ return prev.Date.Year == curr.Date.Year &&
prev.Date.Month == curr.Date.Month; });
}
}
And the implementation of the class becomes:
public class VersementObject : ObjListManager<VersementObject>
{
public int ID
{
get { return _id; }
set { _id = value; }
}
public override DateTime Date
{
get { return _date; }
set { _date = value; }
}
public override object Clone()
{
VersementObject obj = new VersementObject();
return obj;
}
protected override void Combine(VersementObject obj)
{
}
public static new List<VersementObject>
EliminateSameDates(List<VersementObject> listOrig)
{
List<VersementObject> list =
ObjListManager<VersementObject>.EliminateSameDates(listOrig);
int vId = 0;
foreach (VersementObject vo in list)
{
vo.ID = vId++;
}
return list;
}
}
And finally the way I invoke the method:
list = VersementObject.EliminateSameDates(list);
Neat. So that solved my problems. Including the last one. If I need to do some special processing of the list, after it got sorted and prepared, I can do it in the
EliminateSameDates
function by overriding it with a
new
implementation. In the code I have a property
ID
that needs to be set to the value of the position of the object in the list.
As a bonus I also implemented an
EliminateSameMonths
method. This one eliminates all entries from the list that have their date in the same months. All classes inheriting from my
ObjListManager<>
have the two methods:
EliminateSameDates
and
EliminateSameMonths
, so if the logic of the application changes in time (for example all entries in the same
months and not the same
date are now to be removed), you can simply switch the calls.
The two
Eliminate-
methods are very similar so I had them both call the same private method,
Eliminate
, with different elimination criteria. The criterion for elimination is supplied using a delegate. I also used anonymous methods for the implementation of the delegates to keep the code short and compact.
After running my code successfully and the feeling of surprise and relief slowly fading away, another shadow of doubt entered my mind. What if it all works because I have an
abstract
class? Maybe the
abstract
-ness helps avoid the recursion I was worried about. So I tried it without having an abstract class. Well, it worked flawlessly again. Nice.
Points of Interest
The Generics in C# 2 are pretty nice, even though they are no match for the templates in C++. Anyway, they both have pros and cons. Today, the C# compiler surprised me. I have underestimated it. Anyway, the conclusion of my little experiment is that C#'s Generics is a pretty powerful tool to optimize and embellish your code and that I should never stop at the "classic" solution. There's always an even better solution at hand.
History
21 March 2008 - First version of article.