Что такое метаданные? Это некоторая описательная информация, которая используется алгоритмами программы. Например, это могут быть названия таблиц и полей базы данных, названия ключей реестра, в которых хранятся требуемые параметры, или названия свойств объектов, к которым обращается программа. Я хочу рассказать, как с помощью методов-расширений и атрибутов можно удобно использовать перечисления для представления метаданных.
Некоторое время назад я разрабатывал плагин, реализующий некоторый специфичный функционал для популярной системы управления документами. В частности, было необходимо отображать список документов, отвечающих определенным условиям. Для формирования списка документов нужно было сформировать запрос к серверу на встроенным языке (некоторое подмножество XPath). Запрос строится динамически, в зависимости от набора параметров, при этом некоторые параметры могут быть не указаны. Поэтому использование статической строки с подстановками было неприемлемо. Как можно решать эту проблему? Самый простой способ – использовать строковые значения, соответствующие поисковым атрибутам и условиям непосредственно в функции, генерирующей запрос:
Я считаю такой вариант вполне применимым в некоторых сценариях (не нужно кидать в меня гнилыми яблоками). Например, если делается простенькая утилитка, оперирующая двумя атрибутами, которые упоминаются по одному разу и только в одной функции – то городить огород из метаданных может оказаться неуместно. Но если запрос состоит из десятка условий, да еще и запросы бывают разные, то описанный подход, мягко говоря, неуместен. Я бы выделил несколько причин, по которым считаю этот подход неудачным:
Что же делать? Первое, самое напрашивающееся решение – использовать константы для имен атрибутов, и форматированные строки для описания условий. Результат будет выглядеть примерно так:
Конечно, полученный код легче поддерживать, в него проще вносить изменения, но не могу сказать, что он стал более читаемым. Немного подумав, я нашел решение, которым, собственно, и хотел поделиться. Детали раскрою ниже, а пока просто пример кода генерации запроса (из production кода):
На мой взгляд, этот код достаточно понятен. Поскольку все синтаксические конструкции языка запросов оформлены в виде методов, то синтаксис запроса контролируется на этапе компиляции. Полученный код легко читается и легко изменяется. А добиться такого кода мне удалось используя перечисления, расширения и атрибуты.
Итак, вместо строковых констант, я оформил поисковые поля в виде перечисления. Строковое имя поискового атрибута (оно имеет сложночитаемый XPath-подобный вид)содержится в атрибуте Description:
Каждое элементарное условие на атрибут оформляется в виде метода расширения для этого перечисления. Поскольку каждая строка для условия используется только один раз, то я не стал выносить их в отдельные константы, а использую прямо в функции. Вот пример некоторых из таких функций:
Ну, и еще один метод расширения – который извлекает фактическое имя атрибута:
Этим примером я хотел показать, что в некоторых случаях использовать перечисления гораздо удобнее, чем строковые константы. Перечисление – это явно определенный тип, в то время как строковые константы для компилятора все на одно лицо. Для каждого перечисления можно объявить методы, специфичные только для данного типа. Это дает возможность контролировать правильность использования констант еще на этапе компиляции. Также с помощью атрибутов можно снабдить каждый элемент перечисления дополнительной информацией, которая может использоваться различными методами. Например, в той задаче, которую приводил в качестве примера, в конце концов названия некоторых поисковых полей пришлось поместить в конфигурационный файл. Соответствующие этим полям элементы перечисления получили дополнительный атрибут, содержащий ключ записи в конфигурационном файле. Теперь функция получения имени реквизита проверяет наличие такого атрибута и, при необходимости, обращается к конфигурационному файлу. При этом код генерации строки запроса не претерпел никаких изменений – т.е. сохранены все плюсы использования констант.
Задача о генерации запроса
Некоторое время назад я разрабатывал плагин, реализующий некоторый специфичный функционал для популярной системы управления документами. В частности, было необходимо отображать список документов, отвечающих определенным условиям. Для формирования списка документов нужно было сформировать запрос к серверу на встроенным языке (некоторое подмножество 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;
}
}
Заключение
Этим примером я хотел показать, что в некоторых случаях использовать перечисления гораздо удобнее, чем строковые константы. Перечисление – это явно определенный тип, в то время как строковые константы для компилятора все на одно лицо. Для каждого перечисления можно объявить методы, специфичные только для данного типа. Это дает возможность контролировать правильность использования констант еще на этапе компиляции. Также с помощью атрибутов можно снабдить каждый элемент перечисления дополнительной информацией, которая может использоваться различными методами. Например, в той задаче, которую приводил в качестве примера, в конце концов названия некоторых поисковых полей пришлось поместить в конфигурационный файл. Соответствующие этим полям элементы перечисления получили дополнительный атрибут, содержащий ключ записи в конфигурационном файле. Теперь функция получения имени реквизита проверяет наличие такого атрибута и, при необходимости, обращается к конфигурационному файлу. При этом код генерации строки запроса не претерпел никаких изменений – т.е. сохранены все плюсы использования констант.