I am trying to find a good, clean design template or a common implementation to deal with type enumerations where a single type is known only at runtime.
I know that similar questions have been asked before, but it’s still not clear to me that alternative implementations have significant advantages over a switch or if-thens series.
First, I'm going to demonstrate several implementations, and then I will ask the question: Are these implementations better or preferable than a simple switch? If so, why? If not, why not?
In my application, I send and receive data by stream. At runtime, I get a data structure through serialization that describes which fields are in my binary data. This includes the data type in the field, i.e. Int32, Bool, Double, etc. During development, all I know is that the data can be in one of several types. I need to read fields from a stream and process the data accordingly.
If type inclusion is allowed, the solution may be as follows:
Inoperative code:
object ReadDataField(byte [] buff, ref int position, Dictionary<int, Type> fields) { object value; int field = buff[position]; position++; switch(fields[field]) { case typeof(Int32): { value = (Int32)BitConverter.ToInt32(buff, position); position += sizeof(Int32); break; } case typeof(Int16): { value = (Int16)BitConverter.ToInt16(buff, position); position += sizeof(Int16); break; }
In my opinion, this code has the advantage of being simple, easy to read, and easy to maintain.
However, since type inclusion is not available in C #, I implemented the following:
Work code:
enum RawDataTypes { Int32, Int16, Double, Single, etc. } object ReadDataField(byte [] buff, ref int position, Dictionary<int, RawDataTypes> fields) { object value; int field = buff[position]; position++; switch(fields[field]) { case RawDataTypes.Int32: { value = (int)BitConverter.ToInt32(buff, position); position += sizeof(int); break; } case RawDataTypes.Int16: { value = (Int16)BitConverter.ToInt16(buff, position); position += sizeof(Int16); break; }
This clearly works, but it is also simple and easy to maintain.
However, there are several articles detailing the inclusion of types that are not available in C #. Besides the difficulty of inheriting in a way that gives the expected result, etc., I saw many answers that said that there was a “better” approach that was more in line with the spirit of object-oriented programming.
The general proposed solutions are: 1) the use of polymorphism or 2) the use of dictionary search. But the implementation has its own problems.
Regarding polymorphism, the following is an example of the code “it wouldn’t be nice if it worked”:
Non-operational implementation of polymorphism:
object ReadDataField(byte [] buff, int position, Dictionary<int, Type> fields) { int field = buff[position]; position++; object value = Activator.CreateInstance(fields[field]);
If you try to compile the above code, you will get:
'object' does not contain a definition for ReadRawData and the best overload method 'OverDataFieldExtensions.ReadRawData (short, byte [], ref int)' has some invalid arguments in blah blah ...
You cannot subclass raw data types to add functionality because they are sealed, so extension methods looked like an option. However, extension methods will not convert from an “object” to an actual type, even if a call to value.GetType () returns the base type: System.Int32, System.Int16, etc. Using the keyword "dynamic" does not help either, because you cannot use extension methods for the dynamic type .
The above can be made to work by passing an instance of the object itself as a parameter to methods with polymorphic parameters:
Working implementation of polymorphism:
object ReadDataField(byte [] buff, int position, Dictionary<int, Type> fields) { int field = buff[position]; position++; dynamic value = Activator.CreateInstance(fields[field]); // Here the object is passed to an overloaded method. value = ReadRawData(value, buff, ref position); return value; } public static Int32 ReadRawData(Int32 value, byte[] buff, ref int position) { value = BitConverter.ToInt32(buff, position); position += sizeof(Int32); return value; } public static Int16 ReadRawData(Int16 value, byte[] buff, ref int position) { value = BitConverter.ToInt16 (buff, position); position += sizeof(Int16 ); return value; } // Additional methods for each type...
The above code works and is still simple and maintained, and probably more "in the spirit of object-oriented programming."
But is it really "better?" I would say that this makes it difficult to maintain, as it requires more searching to see which types have been implemented.
An alternative approach is to use dictionary search. Such code might look like this:
Dictionary implementation:
delegate object ReadDelegate(byte [] buff, ref int position); static Dictionary<Type, ReadDelegate> readers = new Dictionary<Type, ReadDelegate> { { typeof(Int32), ReadInt32 }, { typeof(Int16), ReadInt16 }, // Etc... }; object ReadDataField(byte [] buff, int position, Dictionary<int, Type> fields) { int field = buff[position]; position++; object value = readers[fields[field]](buff, ref position); return value; } public static object ReadInt32(byte[] buff, ref int position) { Int32 value = BitConverter.ToInt32(buff, position); position += sizeof(Int32); return value; } public static object ReadInt16(byte[] buff, ref int position) { return BitConverter.ToInt16(buff, position); position += sizeof(Int16); return value; } // Additional methods for each type...
The advantage of the implementation of the dictionary, in my opinion, over polymorphic solutions is that it lists all types that can be processed in one convenient place for reading. It is useful for maintainability.
However, given these examples, are there better, cleaner, more acceptable, etc. implementations that have a significant advantage over the above? Are these implementations using polymorphism or dictionary search preferable to using a switch? I really don’t save any code, and I’m not sure that I have increased the maintainability of the code at all.
In any case, I still need to list each of the types with my own method. Polymorphism postpones the conditional relation to the language itself, instead of being explicit with a switch or if-then. The use of the dictionary is based on internal conditions in order to perform your own search. At the end of the day, what's the difference?