Представление метаданных с помощью перечислений

Что такое метаданные? Это некоторая описательная информация, которая используется алгоритмами программы. Например, это могут быть названия таблиц и полей базы данных, названия ключей реестра, в которых хранятся требуемые параметры, или названия свойств объектов, к которым обращается программа. Я хочу рассказать, как с помощью методов-расширений и атрибутов можно удобно использовать перечисления для представления метаданных.


Задача о генерации запроса


Некоторое время назад я разрабатывал плагин, реализующий некоторый специфичный функционал для популярной системы управления документами. В частности, было необходимо отображать список документов, отвечающих определенным условиям. Для формирования списка документов нужно было сформировать запрос к серверу на встроенным языке (некоторое подмножество XPath). Запрос строится динамически, в зависимости от набора параметров, при этом некоторые параметры могут быть не указаны. Поэтому использование статической строки с подстановками было неприемлемо. Как можно решать эту проблему? Самый простой способ – использовать строковые значения, соответствующие поисковым атрибутам и условиям непосредственно в функции, генерирующей запрос:

string BuildQuery()
{
 ...
 if (IsDateFromSpecified)
  strQuery += "DateFrom >= '" + DateFromValue.ToString() + "'";
 ...
}


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

  • Мы не используем возможности компилятора для контроля корректности запроса – соединять можно любые строки и в любой последовательности.
  • Тяжело читаемый код – “не виден” запрос
  • Есть вероятность ошибки в строках при повторном использовании атрибутов
  • Трудно переименовывать атрибут – нет гарантии, что переименовали во всех местах
  • Трудно переходить на другой синтаксис (например, с XPath на SQL)

Выделяем константы


Что же делать? Первое, самое напрашивающееся решение – использовать константы для имен атрибутов, и форматированные строки для описания условий. Результат будет выглядеть примерно так:

const string MORE_OR_EQUALS_EXPRESSION = "{0} >= '{1}'";
const string DATE_FROM_ATTRIBUTE = "DateFrom";

string BuildQuery()
{  
  ...  
  if (IsDateFromSpecified)    
    strQuery += string.Format(MORE_OR_EQUALS_EXPRESSION, DATE_FROM_ATTRIBUTE, DateFromValue.ToString());  
  ...
}



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

//поток - работы
predicate.Add(DGA.FlowId.EqualsTo(WorksFlowId));
//работа привязана к текущему договору
predicate.Add(DGA.DogovorOfWorkID.EqualsTo(CurrentDocument.Id));
//интервалы
if (DateFrom != null)
  predicate.Add(DGA.WorkEndsAt.MoreOrEqualThan(DateFrom.Value));

if (DateTo != null)
  predicate.Add(DGA.WorkStartsAt.LessOrEqualThan(DateTo.Value));

//только активные работы
if (OnlyActive)
  predicate.Add(DGA.WorkState.EqualsTo(WorkState.Planned.ToString()));



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

Используем перечисление


Итак, вместо строковых констант, я оформил поисковые поля в виде перечисления. Строковое имя поискового атрибута (оно имеет сложночитаемый XPath-подобный вид)содержится в атрибуте Description:

public enum DGA
{
  [Description("doc_RegCard/rc_Index/date_Дата_регистрации")]
  RegDate,
  
  [Description("doc_RegCard/rc_Index/text_Регистрационный_номер")]
  RegNum,
  
  [Description("doc_RegCard/rc_Index/text_Идентификатор_менеджер_договора")]
  Manager,
  
  [Description("doc_RegCard/rc_FlowKey")]
  FlowId,
  
  [Description("doc_RegCard/rc_Index/text_Наименование_договора")]
  Title,
  
  [Description("doc_RegCard/rc_Index/text_Предмет_договора")]
  Subject,
...
}



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

public static string LessOrEqualThan(this DGA attrib, DateTime val)
{
  return attrib.LessOrEqualThan(System.Xml.XmlConvert
    .ToString(val, System.Xml.XmlDateTimeSerializationMode.Unspecified));
}

public static string LessOrEqualThan(this DGA attrib, string val)
{
  return String.Format("{0} <= '{1}'", attrib.GetAttribName(), val);
}

public static string InList(this DGA attrib, IEnumerable<string> values)
{
  return String.Format("{0} in list({1})",
    attrib.GetAttribName(), values
      .Select(x => "'" + x + "'")
      .Aggregate((x, y) => x + ", " + y));
}



Ну, и еще один метод расширения – который извлекает фактическое имя атрибута:

public static class DGAExt
{
  public static string GetAttribName(this DGA attrib)
  {
    if (_dgaNames != null)
      return _dgaNames[attrib];
  
    return EnumHelper.GetDescription(attrib);
  }
}

public static class EnumHelper
{
  /// <summary>
  /// Retrieve the description on the enum, e.g.
  /// [Description("Bright Pink")]
  /// BrightPink = 2,
  /// Then when you pass in the enum, it will retrieve the description
  /// </summary>
  /// <param name="en">The Enumeration</param>
  /// <returns>A string representing the friendly name</returns>
  public static string GetDescription(Enum en)
  {
    var desc = GetAttribute<DescriptionAttribute>(en);
    if (desc != null)
      return desc.Description;
  
    return en.ToString();
  }

  public static T GetAttribute<T>(Enum en) where T : System.Attribute
  {
    Type type = en.GetType();

    MemberInfo[] memInfo = type.GetMember(en.ToString());

    if (memInfo.Length > 0)
    {
      var attrs = memInfo[0].GetCustomAttributes(typeof(T), false).Cast<T>();
      return attrs.FirstOrDefault();
    }

    return null;
  }
}



Заключение



Этим примером я хотел показать, что в некоторых случаях использовать перечисления гораздо удобнее, чем строковые константы. Перечисление – это явно определенный тип, в то время как строковые константы для компилятора все на одно лицо. Для каждого перечисления можно объявить методы, специфичные только для данного типа. Это дает возможность контролировать правильность использования констант еще на этапе компиляции. Также с помощью атрибутов можно снабдить каждый элемент перечисления дополнительной информацией, которая может использоваться различными методами. Например, в той задаче, которую приводил в качестве примера, в конце концов названия некоторых поисковых полей пришлось поместить в конфигурационный файл. Соответствующие этим полям элементы перечисления получили дополнительный атрибут, содержащий ключ записи в конфигурационном файле. Теперь функция получения имени реквизита проверяет наличие такого атрибута и, при необходимости, обращается к конфигурационному файлу. При этом код генерации строки запроса не претерпел никаких изменений – т.е. сохранены все плюсы использования констант.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 14

    +2
    Спасибо! Мне тоже кажется работа с «enum + аттрибуты» часто более удобной.

    > Я бы выделил несколько причин, по которым считаю этот подход неудачным:
    Я бы добавил к минусам следующее:
    strQuery += "DateFrom >= '" + DateFromValue.ToString() + "'";
    С immutable-классами так работать нельзя. Здесь лучше использовать StringBuilder и string.Format("",...).

    А в плюсы энумок добавил бы упрощение локализации через аттрибуты. К примеру, можно для значений указать аттрибуты вида «LocalizableFriendlyName» и через extension-methods возвращать для конкретного значения перечисления локализованное дружественное имя и какого-либо ресурса.
      0
      >С immutable-классами так работать нельзя. Здесь лучше использовать StringBuilder и string.Format("",...).
      наверное так будет лучше, ну сути это не меняет.
      0
      Мне кажется, уже есть стандартный для вашего случая формат метаданных — Expressions. И с помощью PredicateBuilder легко получить такие «собираемые предикаты» и потом на этот полученный Expression можно скормит Entity Framework, LINQ, или даже для некоторого несложного подмножества написать свою реализацию репозитория, разбирающего выражение и делая запрос на основе него к произвольному хранилищу.
        0
        Да, еще бывают Expression. В следующий раз попробую воспользоваться ими. Данное решение получилось путем эволюционного перехода от констант и показалось мне интересным.
          0
          Понятно, ну да :)
        0
        Спасибо KvanTTT за совет — добавил расцветку для кода
          0
          Лишние переносы строк в примерах кода уберите, пожалуйста.
            0
            Вы знаете, в тексте нет переносов. Предпросмотр перед публикацией показывал тексты нормально, а после публикации — выглядит вот так странно. Как бороться, к сожалению, не знаю, но буду благодарен за любой совет.
            0
            Справился с оформлением кода. Оказывается хабр умеет сам подсвечивать исходники, однако это не сразу найдешь.
              0
              У вас есть конкретный enum DGA, к которому вы добавили конкретные extensions DGAExt. Зачем использовать медленный reflection, когда можно было просто сделать в классе DGAExt
              private Dictionary<DGA, string> attributeNames = new Dictionary<DGA, string>
              {
              { DGA.RegDate, «doc_RegCard/rc_Index/date_Дата_регистрации» },
              { DGA.RegNum, «doc_RegCard/rc_Index/text_Регистрационный_номер» },

              };

              public static string GetAttribName(this DGA attrib)
              {
              return this.attributeNames[ attrib ];
              }

              Либо вообще сделать attributeNames public и обойтись без метода, хотя метод позволяет добавить валидацию в случае необходимости.

              Без reflection скорость выше, понятность в худшую сторону не меняется, весь мэппинг аналогично находится в одном месте, просто не в декларации самого enum.
                0
                Да, наверное в случае когда нужно привесить одну строку такой вариант можно использовать.
                Разница, наверное, не столько в читаемости, сколько в гарантии неизменности словаря.
                Хотя был случай, когда я как раз такой словарь и использовал, а наполнялся он в процессе работы программы.

                Насчет производительности ничего не могу сказать, но я не думаю, что конкретно в этом случае reflection сильно медленнее словаря, надо будет померять при случае.
                  0
                  Почему «привесить одну строку»? Как ваш атрибут можно расширять свойствами, так и мэппинг можно делать не в строку, а в сложный класс, имеющий все небходимые свойства, либо делать набор мэппингов — по вкусу.
                  Что касается неизменяемости словаря, что вас смущает, если он скрыт, а доступ организован через метод?
                    0
                    *Долго думал
                    Давайте чуть-чуть проанализируем.
                    Чем плохо использование атрибутов по сравнению со словарем?
                    Судя по всему — только производительностью

                    Чем плохо использование словаря по сравнению с атрибутами?
                    На этот вопрос я долго не мог ответить даже для себя. Было только подсознательное ощущение какой-то неправильности. Наконец осознал. Проблема чисто организационная — информация о присоединенных атрибутах находится в стороне от объявления элементов перечисления. Таким образом, если когда кто-то будет добавлять элементы перечисления, то он может не сходу увидеть, что атрибуты добавляются где-то еще. Т.е. имеет место небольшое нарушение принципа DRY: каждый элемент перечисления нужно продублировать в инициализаторе словаря.

                    Вариант решения
                    В итоге мне кажется разумным использовать комбинированный вариант, а именно, писать метаданные в атрибуты, но завести словарь, информация в который загружается в процессе выполнения(например, в статическом конструкторе класса-расширения). Это обеспечит нужную производительность и сохранит прозрачность кода.
                      0
                      Да, вариант кэширования в мэппинге хорош, я об этом уже тоже думал.

              Only users with full accounts can post comments. Log in, please.