.comment-link {margin-left:.6em;}

I Hate Linux

Friday, February 09, 2007

Reflectaholism

Hi, my name is Brendan... and I'm a reflectaholic.

This is my first meeting... I first discovered that I had reflectaholism, the desire to use reflection anytime and anywhere when I tried to discreetly examine the properties and methods of my date and later attempted to invoke some of her methods.

Needless to say... it didn't end well.

As many know, I love .NET, it's the kind of thing that makes me stay awake at night and makes writing new code even more exciting (I was always thrilled about writing code but when I got into .NET 4 years ago it only grew).

What is my favorite feature? Reflection. In fact... I'm the sort of guy that loves it so much that he can and often will come up with an excuse to use it just for the hell of it.

Recently such a case came up... within a DNS server I'm writing I had a large switch statement that looked like so:
         switch (Type)
{
case RecordType.A:
Record = new ARecord(reader);
break;
case RecordType.AAAA:
Record = new AaaaRecord(reader);
break;
case RecordType.CNAME:
Record = new CNameRecord(reader);
break;
case RecordType.HINFO:
Record = new HInfoRecord(reader);
break;
case RecordType.MX:
Record = new MxRecord(reader);
break;
case RecordType.NS:
Record = new NsRecord(reader);
break;
case RecordType.PTR:
Record = new PtrRecord(reader);
break;
case RecordType.RP:
Record = new RpRecord(reader);
break;
case RecordType.SOA:
Record = new SoaRecord(reader);
break;
case RecordType.SPF:
Record = new SpfRecord(reader);
break;
case RecordType.TXT:
Record = new TxtRecord(reader);
break;
default:
Console.WriteLine("Unknown Type: " + Type.ToString());

//Skip record
short length = reader.ReadShort();
reader.BaseStream.Position += length;
break;
}
Ewww! There must be a better way. A way that doesn't involve a lengthy switch statement that could go on forever... for an experiment I wrote the following in its place:
         Dictionary<RecordType, Type> types = new Dictionary<RecordType, Type>();
types.Add(RecordType.A, typeof(ARecord));
types.Add(RecordType.AAAA, typeof(AaaaRecord));
types.Add(RecordType.CNAME, typeof(CNameRecord));
types.Add(RecordType.HINFO, typeof(HInfoRecord));
types.Add(RecordType.MX, typeof(MxRecord));
types.Add(RecordType.NS, typeof(NsRecord));
types.Add(RecordType.PTR, typeof(PtrRecord));
types.Add(RecordType.RP, typeof(RpRecord));
types.Add(RecordType.SOA, typeof(SoaRecord));
types.Add(RecordType.SPF, typeof(SpfRecord));
types.Add(RecordType.TXT, typeof(TxtRecord));

if (types.ContainsKey(Type))
{
Type t = types[Type];
object o = Activator.CreateInstance(t, reader);
Record = o as Record;
}
else
{
Console.WriteLine("Unknown Type: " + Type.ToString());

//Skip record
short length = reader.ReadShort();
reader.BaseStream.Position += length;
}
Shorter... but still not there.

I should pause real quick and explain a bit... a DNS message contains several lists of what are called Resource Records (RR). Each RR itself has a Record field that can be one of any of the defined DNS types, an MX record (mail server), an A record (ip address of host name), NS record (name server), etc.

So that the reader of the RR knows what kind of record it is going to encounter, there is a field on the RR which defines this and which can be represented easily in code as an enum... only kicker is coming up with some kind of mapping between the value of the enum and type of class (Record) that should be created to handle the provided data.

In the first example, it's simply a brute force statement asking the repeating question "Is it this? No... is it this? No... is it this? Yes... then make this one."

This works fine and dandy... but IMO is ugly as all heck and requires extra work when changes are needed.

The second implementation used a dictionary to hold the mappings between the enum value and the class type... this way when I have the enum value, I can simply ask the dictionary "which type is associated with this value?" and leave the work up to it and once I know the type, I can simply say "Make me an instance of this type" without ever having to manually specify it.

Far cleaner and smaller... but still ugly as it requires explicit filling of the dictionary. There still must be another way... a way that reduces the centralized work required to add a new record type... which is where I came up with this monstrosity:

      public Dictionary<RecordType, Type> PopulateDictionary()
{
Dictionary<RecordType, Type> table = new Dictionary<RecordType, Type>();
Assembly asm = Assembly.GetEntryAssembly();

AssemblyName[] names = asm.GetReferencedAssemblies();

foreach (AssemblyName name in names)
{
Assembly a = Assembly.Load(name);

Type[] types = a.GetTypes();

foreach (Type t in types)
{
if( InheritsFrom(t, typeof(Record)))
{
FieldInfo fi = t.GetField("Type");

if (fi != null)
{
RecordType rt = (RecordType)fi.GetValue(null); ;

table.Add(rt, t);
}
}
}

}

return table;
}

private bool InheritsFrom(Type super, Type potentialBase)
{
while (super.BaseType != null)
{
if (super.BaseType == potentialBase)
return true;

super = super.BaseType;
}

return false;
}
In the above, I use reflection to populate the dictionary for us by iterating through each assembly currently loaded by the application and then examine each class type, if an class inherits from a known base class (which all of the record classes in my code inherit from), then we add it.

Before adding it though... it needs to know what enum type this type of class relates to... in order to accomplish this I require each inheritor of the base class to define on it's own a variable named Type which defines this... this way, each class is capable of telling the world what value it maps to.

Once our dictionary is populated, we get our values from it just the same was as in example 2.

Why did I do this? One morning on the way to work I began to ponder better ways of doing a few things in the server and these horrors came to me.

Funny thing though... when adding a new record, using #2 requires less work than #1, just as #3 makes it easier than #2 or #1... only #3 comes with a major up front performance hit.

In the current version of the server I'm still using #1 as I haven't sat down to do any serious profiling and as for the time being... I've got better areas to focus on... like improving the efficiency of my caching.

1 Comments:

  • I thought reflection would be, you know, elegant and efficient and stuff like that.

    By Blogger Daniel, at 7:33 AM  

Post a Comment

<< Home