понедельник, 30 мая 2011 г.

Улучшаем перечисления

В основу публикации положена статья Dieter Deysel Using Attributes, Reflection and Generics To Increase The Usability Of Enums.

Данная статья описывает решение, используемое нашей командой в разрабатываемых проектах. В решении используются кастомные аттрибуты, механизм отражения и дженерики. С их помощью мы получаем максимальную отдачу от использования перечислений, наделяя каждый элемент перечисления дополнительными метаданными. Надеюсь многим будет интересно познакомиться с нашим решением. Уверен, оно позволит упростить и улучшить разрабатываемый код.
В текущем проекте нам приходится использовать большое количество разнообразным перечислений различных типов. Использование перечислений само по себе очень упрощает код и избавляет от всякого хардкода. Однако, иногда элементы перечисления очень хочется дополнить экстра данными, как например строковое описание элемента или что то еще. Ниже я хочу продемонстрировать, как мы решили этот момент.
Многие разработчики наверняка видели реализации подобной функциональности с использованием самодельного аттрибута EnumDescription, который может к элементу перечисления привязать строковое описание поясняющее его суть. Вот пример такого использования аттрибута:

public enum Institution
{
    [EnumDescription(“Bank A”)]
     BankA,
 
    [EnumDescription (“Shop B”)]
     ShopB,
    [EnumDescription(“Library C”)]
     LibraryC
}

Наше решение основано на похожей концепции. Мы используем расширенные аттрибуты, которые содержат не просто одно строковое поле для описания элемент, а множество всех необходимых свойств. Помимо этого мы используем не один, а несколько кастомных аттрибутов, для семантического разделения их предназначений. Такой подход, позволяет более гибко декорировать те или иные элементы перечислений.
Вот пример реализации наших аттрибутов:

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class InstituteAttribute: Attribute
{
    public InstituteAttribute(string Desc, string Phone)
    {
        Description = Desc;
        Telephone=Phone;
    }             
    public string Description {get;set;}
    public string Telephone {get;set;}
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class AddressAttribute: Attribute
{
     public AddressAttribute (string Add1, string Add2, string City, string ZipCode)
     {
         this.Address1 = Add1;
         this.Address2 = Add2;
         this.City = City;
         this.ZipCode = ZipCode;
     }             
     public string Address1 {get;set;}
     public string Address2 {get;set;}
     public string City {get;set;}
     public string ZipCode {get;set;}
}

Для того что бы связать дополнительные данные с элементом перечисления, мы используем подход схожий с тем, который который применяется в случае описанном выше с аттрибутом EnumDescription. Но применение наших аттрибутов не является таким же гибким, так как мы должны знать, с каким типом мы работаем, и какие свойства аттрибутов доступны.
Вот пример использования наших аттрибутов.

public enum Institution
{
    [Institute (“Bank A”,  ”(856) 745-1546”)]
    [Address(“123 Street”, “Some town”, “Some City” , ”1452”)]
     BankA,
 
    [Institute (“Shop B”,  “(012) 154-1456”)]
     ShopB,
   
    [Institute (“Library C”,  “(123) 147-4567”)]
     LibraryC
}

Для того что бы получить данные из аттрибутов, мы реализовали метод расширения, который используя средства рефлексии, получает дополнительую информацию из свойств аттрибутов, которые есть у данного элемента перечисления.
Ниже представлена реализация подобного метода:

///
/// Use this extension method to obtain any property in an attribute used to decorate the fields of an Enum.
///
/// Type of attribute containing the wanted properties
/// This will be the Enum value
/// The property of the specified attribute that must be returned.
/// The property value specified
public static string GetPropertyValue(this Enum value, string AttributeProperty)
{
    //Get the enum value
    FieldInfo fi = value.GetType().GetField(value.ToString());
   
    T[] atts = fi.GetCustomAttributes(typeof(T), false) as T[];
    //if there is any attributes of the specified type
    if (atts.Length == 0)
    return string.Empty;

    //find propertyname
    PropertyInfo pi = atts[0].GetType().GetProperty(AttributeProperty);
    if (pi == null)
    throw new ArgumentException(String.Format("{0} is not a valid property name.", AttributeProperty));

    //return value of property
    return pi.GetValue(atts[0],null).ToString();
}

Теперь, обладая подобным методом, мы без труда можем получить дополнительные данные элемента перечисления.
Ниже представлен пример получения данных: 

string shopPhone = Institution.ShopB.GetPropertyValue(“Telephone”);

Выражение в примере выше вызовет метод расширения GetPropertyValue для элемента перечисления ShopB и вернет значение свойства Telephone аттрибута Institute, который есть у элемента ShopB.
Вот и все. Надеюсь описанное здесь решение окажется полезным и для вас.