James Michael Hare

...hare-brained ideas from the realm of software development...
posts - 166 , comments - 1431 , trackbacks - 0

My Links


Welcome to my blog! I'm a Sr. Software Development Engineer in the Seattle area, who has been performing C++/C#/Java development for over 20 years, but have definitely learned that there is always more to learn!

All thoughts and opinions expressed in my blog and my comments are my own and do not represent the thoughts of my employer.

Blogs I Read

Follow BlkRabbitCoder on Twitter

Tag Cloud


Post Categories



Little Wonders

Little Wonders


C# Toolbox: Building a Loosely Coupled Translator

In my last fundamentals post, Chuck had requested an example of how to translate between enum and int without resorting to casting or other hard-coded mechanisms that depend on the actual values of the enum.  One of the problems, of course, with casting between enum and int (for example to represent the enum as an int in database) is that it is a very tightly-coupled bond.  Any changes to the underlying data or to the enum could have disastrous consequences. 

Thus, it is often more desirable to have a more losely-coupled translator that can translate from enum to int and back.  To take this further, this same concept can be applied to translate between, really, any type and any other type.

So what is the difference between translation and conversion?  Well, maybe this is picking semantic nits, but I consider conversion to be when you can infer a converted value directly from the original by applying a formula.  For example, I know I can convert "1032" => 1032 by parsing each character and building an integer with its values (of course, .NET already provides that functionality in int.Parse() but you get my point.

So then what is a translation?  To me a translation is the opposite: it's when the translated value cannot be deduced from the original value by a direct formula, for example say you have an account type of 123 and that represents a Checking account.  There's no way to deduce via formula that 123 => Checking.

So how do we resolve this?  Well, there are several ways to do this -- far more than mentioned in this post -- and everyone has their favorite way to handle these.  You can, of course, always just load up a Dictionary or SortedList with what you want.  Let's say that we have a series of two-character account codes that are in an external data source that we want to convert to an internal enumeration:

   1: public enum AccountType
   2: {
   3:     Checking,
   4:     Savings,
   5:     CertificateOfDeposit,
   6:     HomeEquityLineOfCredit,
   7:     Unknown
   8: }

Now, we could easily load a Dictionary with our translations.  This can either be loaded from a data store, or if it's a fairly static list you can just load it during construction and make it static since all the collection classes are thread-safe for read operations.

   1: // data access object for loading accounts
   2: public class AccountDao
   3: {
   4:     // a translation table for account code to account type
   5:     private static readonly IDictionary<string, AccountType> _translations 
   6:         = new Dictionary<string, AccountType>
   7:             {
   8:                 { "1A", AccountType.Checking },
   9:                 { "1S", AccountType.Savings },
  10:                 { "CD", AccountType.CertificateOfDeposit },
  11:                 { "HL", AccountType.HomeEquityLineOfCredit }
  12:             };
  13:     ...
  14: }

Once you have the dictionary loaded, you can easily query it at any time by giving it the string value to translate:

   1: AccountType convertedType = _translations["1A"];



Now, the only problem with using the Dictionary for this is that any lookups that fail will throw an exception unless you use the TryGetValue() method.  Now, while this is good behavior for a dictionary as a whole, for translations, I would prefer to make it a little more forgiving.  Which leads me to this little translation helper.  It's a simple generic class that can wrap any IDictionary implementer and have a default readily available if no translation exists.

   1: // Translates from objects of type TFrom to type TTo
   2: public class Translator<TFrom, TTo, TDictionary> : IEnumerable<KeyValuePair<TFrom, TTo>>
   3:     where TDictionary : IDictionary<TFrom, TTo>, new()
   4: {
   5:     // The translation table
   6:     private readonly IDictionary<TFrom, TTo> _map;
   8:     // Property to get/set the default to value if a lookup fails
   9:     public TTo DefaultValue { get; set; }
  11:     // Property that returns the number of elements in the lookup table
  12:     public int TranslationCount
  13:     {
  14:         get { return _map.Count; }
  15:     }
  17:     // Indexes the translator.  On get it will lookup the type calling
  18:     // Translate.  On set it will Add a new lookup with the given value.
  19:     public TTo this[TFrom index]
  20:     {
  21:         get { return Translate(index); }
  22:         set { Add(index, value); }
  23:     }
  25:     // Create a translator using system defaults for the type (NULL if reference
  26:     // type and ZERO for value types)
  27:     public Translator()
  28:         : this(default(TTo))
  29:     {
  30:     }
  32:     // Create a translator with a default to and from value specified
  33:     public Translator(TTo defaultValue)
  34:     {
  35:         DefaultValue = defaultValue;
  37:         _map = new TDictionary();
  38:     }
  40:     // Performs a translation using the table, returns the default from value
  41:     // if cannot find a matching result.
  42:     public TTo Translate(TFrom value)
  43:     {
  44:         TTo result;
  46:         // loop through table looking for result
  47:         if (value == null || !_map.TryGetValue(value, out result))
  48:         {
  49:             result = DefaultValue;
  50:         }
  52:         return result;
  53:     }
  55:     // Adds a new translation to the translation table
  56:     public void Add(TFrom key, TTo value)
  57:     {
  58:         _map.Add(key, value);
  59:     }
  61:     // Clears all existing translations and defaults
  62:     public void Clear()
  63:     {
  64:         _map.Clear();
  65:     }
  67:     // Get an enumerator to walk through the list
  68:     public IEnumerator<KeyValuePair<TFrom, TTo>> GetEnumerator()
  69:     {
  70:         return _map.GetEnumerator();
  71:     }
  73:     // Get an enumerator to walk through the list
  74:     IEnumerator IEnumerable.GetEnumerator()
  75:     {
  76:         return GetEnumerator();
  77:     }
  78: }

Now, this is only a simple example of such a class, you could easily also build in capabilities to reverse-translate and so on.  Notice that you can specify a DefaultValue in the constructor, if you don't supply one it will use default(TTo) which will be null for reference types or zero for primatives (including the zeroth value in any enum).  Also, this generic Translator allows you to specify alternative IDictionary implementations so you can tune the performance or behavior to your liking (SortedList, SortedDictionary, Dictionary, etc). 

Also notice that the class implements IEnumerable and has an Add() method defined.  This allows it to be used with the List Initializer syntax:

   1: // load using traditional methods
   2: var translator = new Translator<string, AccountType>(AccountType.Unknown);
   3: translator.Add("1A", AccountType.Checking);
   4: translator.Add("1S", AccountType.Savings);
   5: translator.Add("CD", AccountType.CertificateOfDeposit);
   6: translator.Add("HL", AccountType.HomeEquityLineOfCredit);
   8: // load using initializer
   9: var othertranslator = new Translator<string, AccountType(AccountType.Unknown)
  10:     {
  11:         { "1A", AccountType.Checking },
  12:         { "1S", AccountType.Savings },
  13:         { "CD", AccountType.CertificateOfDeposit },
  14:         { "HL", AccountType.HomeEquityLineOfCredit }
  15:     };

You can also "overload" a generic class definition by supplying one that takes fewer or more parameters, for example, we could create:

   1: // Same as the generic translator class but, in effect, defaults the Dictionary used to a
   2: // System.Collections.Generic.Dictionary.
   3: public class Translator<TFrom, TTo> : Translator<TFrom, TTo, Dictionary<TFrom, TTo>>
   4: {
   5: }


Yes, this class is empty, it's just a sub-class of the more generic Translator which just passes in a Dictionary to be the default IDictionary implementation.  So you can define:

   1: // create a translator using the default Dictionary (defaults to AccountType.Unknown if not found)
   2: private static readonly Translator<string, AccountType> _translator 
   3:     = new Translator<string,AccountType>(AccountType.Unknown)
   4:         {
   5:             { ... }
   6:         };
   8: // create a translator using a SortedList instead (defaults to AccountType.Unknown if not found)
   9: private static readonly Translator<string, AccountType, SortedList<string, AccountType>> _otherTranslator 
  10:     = new Translator<string, AccountType, SortedList<string,AccountType>>(AccountType.Unknown)
  11:         {
  12:             { ... }
  13:         };



So that's an example of a simple Translator class.  It's really just a glorified Dictionary with a tolerance for non-finds, but it can be as much or as little as you'd like it to be.

The main point is, translations can be very difficult to maintain over time.  You could hard-code all the translations in conditional logic, but this is very rigid and creates a lot of technical debt over time as requirements change.  In some cases like translating between enum and int you can assign the enum values to the int values you need, but this makes the enum tightly coupled to your data sources.  Or, you can create a mapping in a Dictionary or your own custom type which can be loaded dynamically from a data source, a config file, or in the code which is losely coupled, flexible, and quite efficient.

Technorati Tags: ,


 Technorati Tags: , , , ,


Print | posted on Tuesday, July 13, 2010 11:10 PM | Filed Under [ My Blog C# Software .NET Fundamentals Toolbox ]

Powered by: