Как бы вы изменили «переключение на тип», чтобы оно стало полиморфным, если вы не контролируете типы? - PullRequest
8 голосов
/ 16 февраля 2009

Я пишу анализатор микроформатов в C # и ищу совет по рефакторингу. Вероятно, это первый «настоящий» проект, который я пытался сделать на C # в течение некоторого времени (я программирую почти исключительно на VB6 на своей повседневной работе), поэтому я чувствую, что этот вопрос может стать первым в серии; -)

Позвольте мне рассказать о том, что у меня есть, чтобы мой вопрос (надеюсь) имел смысл.

Прямо сейчас у меня есть один класс, MicroformatsParser, делающий всю работу. Он имеет перегруженный конструктор, который позволяет передавать System.Uri или string, содержащий URI: при создании он загружает HTML-документ по указанному URI и загружает его в HtmlAgilityPack.HtmlDocument для удобства манипулирования классом.

Базовый API работает так (или будет, когда я закончу код ...):

MicroformatsParser mp = new MicroformatsParser("http://microformats.org");
List<HCard> hcards = mp.GetAll<HCard>();

foreach(HCard hcard in hcards) 
{
    Console.WriteLine("Full Name: {0}", hcard.FullName);

    foreach(string email in hcard.EmailAddresses)
        Console.WriteLine("E-Mail Address: {0}", email);
}

Использование дженериков здесь является преднамеренным. Я черпал вдохновение в том, как работает библиотека микроформатов в Firefox 3 (и гем Ruby mofo). Идея заключается в том, что синтаксический анализатор выполняет тяжелую работу (находя фактическое содержание микроформатов в HTML), а сами классы микроформатов (HCard в приведенном выше примере) в основном предоставляют схему, которая сообщает анализатору, как обрабатывать данные, которые он обрабатывает. находит.

Код для класса HCard должен прояснить это (обратите внимание, что это не полная реализация):

[ContainerName("vcard")]
public class HCard
{
    [PropertyName("fn")]
    public string FullName;

    [PropertyName("email")]
    public List<string> EmailAddresses;

    [PropertyName("adr")]
    public List<Address> Addresses;

    public HCard()
    { 
    }
}

Атрибуты, используемые здесь, используются синтаксическим анализатором для определения того, как заполнить экземпляр класса данными из документа HTML. Парсер делает следующее при вызове GetAll<T>():

  • Проверяет, что тип T имеет атрибут ContainerName (и он не пустой)
  • Поиск в документе HTML всех узлов с атрибутом class, который соответствует ContainerName. Назовите их «узлами контейнера».
  • Для каждого контейнерного узла:
    • Использует отражение для создания объекта типа T.
    • Получить открытые поля (a MemberInfo[]) для типа T через отражение
    • Для каждого поля MemberInfo
      • Если поле имеет атрибут PropertyName
        • Получить значение соответствующего свойства микроформата из HTML
        • Вставить значение, найденное в HTML, в поле (т.е. установить значение поля для объекта типа T, созданного на первом шаге)
        • Добавьте объект типа T к List<T>
    • Возвращает List<T>, который теперь содержит кучу микроформатных объектов

Я пытаюсь найти лучший способ реализовать шаг в полужирном . Проблема заключается в том, что Type данного поля в классе микроформатов определяет не только какой узел искать в HTML, но также и как интерпретировать данные.

Например, возвращаясь к классу HCard, определенному выше, свойство "email" привязано к полю EmailAddresses, которое является List<string>. После того, как синтаксический анализатор находит все дочерние узлы "email" родительского узла "vcard" в HTML, он должен поместить их в List<string>.

Более того, если я хочу, чтобы мой HCard мог возвращать информацию о номере телефона, я, вероятно, хотел бы иметь возможность объявить новое поле типа List<HCard.TelephoneNumber> (которое будет иметь свой собственный атрибут ContainerName("tel")) чтобы хранить эту информацию, потому что в HTML может быть несколько элементов "tel", а формат "tel" имеет свои собственные подчиненные свойства. Но теперь парсер должен знать, как поместить телефонные данные в List<HCard.TelephoneNumber>.

Та же проблема относится к Float S, DateTime S, List<Float> S, List<Integer> S и т. Д.

Очевидный ответ - переключить анализатор на тип поля и выполнить соответствующие преобразования для каждого случая, но я хочу избежать гигантского оператора switch. Обратите внимание, что я не планирую поддерживать синтаксический анализатор всеми возможными Type в существовании, но я хочу, чтобы он обрабатывал большинство скалярных типов и их версии List<T> вместе с возможностью распознавания других классов микроформатов ( так что класс микроформатов может быть составлен из других классов микроформатов).

Какой-нибудь совет, как лучше всего справиться с этим?

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

Моей первой мыслью было использование перегрузки методов, поэтому у меня была бы серия перегрузок GetPropValue, таких как GetPropValue(HtmlNode node, ref string retrievedValue), GetPropValue(HtmlNode, ref List<Float> retrievedValue) и т. Д., Но мне интересно, есть ли лучший подход к этой проблеме.

Ответы [ 3 ]

5 голосов
/ 16 февраля 2009

Подход Mehrdad в основном тот, который я бы предложил начать, но как первый шаг из потенциально большего.

Вы можете использовать простой IDictionary<Type,Delegate> (где каждая запись на самом деле от T до Func<ParseContext,T> - но это не может быть выражено с помощью обобщений) для отдельных типов (строки, примитивы и т. Д.), Но тогда вы будете также хотите проверить списки, карты и т. д. Вы не сможете сделать это с помощью карты, потому что вам нужно будет иметь запись для каждого типа списка (то есть отдельную запись для List<string>, List<int> так далее). Дженерики делают это довольно сложно - если вы хотите ограничить себя просто определенными конкретными типами, такими как List<T>, вы сделаете это проще для себя (но менее гибким). Например, обнаружить List<T> просто:

if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
    // Handle lists
    // use type.GetGenericArguments() to work out the element type
}

Определить, реализует ли тип IList<T> для некоторого T (и затем обнаруживать T), может быть больно, особенно потому, что может быть несколько реализаций, а сам конкретный тип может или не может быть универсальным. Эти усилия могут быть полезны, если вам действительно нужна очень гибкая библиотека, используемая тысячами разработчиков, но в противном случае я бы оставил ее простой.

4 голосов
/ 16 февраля 2009

Вместо большого оператора switch вы можете создать словарь, который отображает строку в делегат и ищет ее, когда вы хотите проанализировать, используя соответствующий метод.

2 голосов
/ 16 февраля 2009

Это очень похоже на проблему, с которой сталкивается мой механизм сериализации ( protobuf-net ). Я просто разбил его на общие наборы логики - IList<T> и т. Д. (Хотя существует большой тест на логику / тип для обработки различных примитивов). Подход, который я использую, заключается в следующем: сделайте это только один раз ... создайте модель на основе интерфейса / базового класса, которая может обрабатывать свойства различных типов и работать оттуда. Я делаю это в статическом инициализаторе универсального класса кэша - т.е. Foo<T>; когда T равен HCard, я предварительно вычисляю модель (т.е. создаю объект для каждого свойства, который может анализировать / отображать это свойство и сохранять их), что позволяет мне обрабатывать HCard, не задумываясь позже.

Я не говорю, что это лучший код на земле, но, похоже, он работает нормально.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...