Статически определяемые параметры для типа производного класса

2

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

Я начал с абстрактного метода GetOptionsSchema(), например:

public abstract class Widget
{
    public abstract OptionsSchema GetOptionsSchema();
    public abstract void DoStuff(Options options);
}

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

public abstract class FooWidget: Widget
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public overide OptionsSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

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

В конечном счете, я хотел бы иметь возможность определять схему параметров для определенного типа виджета непосредственно из System.Type. Я бы создал собственный атрибут OptionsSchema, но построение этих схем сложнее, чем это имеет смысл сделать в конструкторе атрибута. Это должно произойти в методе.

Я видел, как другие фреймворки решают подобные проблемы, создавая собственный атрибут, который идентифицирует статический метод или свойство по имени. Например, атрибут TestCaseSource в NUnit.

Вот как может выглядеть эта опция:

public abstract class Widget
{
    public abstract void DoStuff(Options options);
}

[OptionsSchemaSource(nameof(GetOptionsSchema))]
public abstract class FooWidget: Widget
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public static OptionSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

Мне нравится, как атрибут OptionsSchemaSource позволяет получать схему параметров непосредственно из System.Type, но это также кажется гораздо менее доступным для других разработчиков, создающих собственные типы виджетов.

С помощью абстрактного метода другой разработчик виджетов знает, что они должны переопределить GetOptionSchema(), потому что их код не скомпилируется иначе. С атрибутом OptionsSchemaSource лучше всего было бы надеяться, что люди прочитают мою документацию и заставят инфраструктуру выдать исключение во время выполнения, если она встретит виджет без атрибута OptionsSchemaSource.

Есть ли альтернативный/лучший/рекомендуемый подход к этому?

Теги:
attributes
static-methods

2 ответа

1
Лучший ответ

Вы в значительной степени уже знаете все, что интересно, чтобы судить, какой подход лучше.

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

Итак, две альтернативы, которые вы определили, являются единственными, о которых я могу думать. Теперь давайте сделаем плюсы и минусы и постараемся отточить их.

атрибут

Вы можете уменьшить трудность обеспечения того, чтобы разработчики помещали атрибуты в классы с осмысленными сообщениями об ошибках. Я бы сказал, что вы должны управлять обнаружением классов, основанных исключительно на Атрибутах, а не на наследовании. Если вы управляете всем с помощью атрибутов, вам не нужно наследовать от виджета. Это профессионал, потому что теперь каждый может наследовать, если это желательно, и повторно внедрять, если предпочитает.

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

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

Абстрактный класс

Что ж, вы навязываете цепочку наследования своим разработчикам, что может быть в порядке, необходимо или совершенно необязательно (судите сами), но вы получаете опыт самодокументирования.

Очевидным доводом является то, что при запуске вам нужно создать экземпляр виджета для каждого обнаруженного вами производного типа, но это вполне может быть арахис по сравнению со сканированием сборки и проверкой типа, а также обнаружениемinfo-информации и вызовами метода через отражение. Гадкий? Вид. Неэффективное? Не так много. И это код, который невидим для вашего конечного пользователя.

по моему мнению

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

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

Мой призыв: я бы пошел по маршруту абстрактного класса с небольшим предостережением. Мне действительно не нравится иметь обязательный базовый класс. Поэтому я бы организовал обнаружение при запуске на основе интерфейса IWidget, содержащего метод GetOptionsSchema и все необходимое для использования виджета (который может быть методом DoStuff, но вполне может быть чем-то другим). При запуске вы ищете реализации интерфейса, которые не являются абстрактными, и вы готовы к работе.

Если и только если единственный бит, который вам действительно нужен заранее, это строка или другой подобный простой тип, мне потребуется дополнительный атрибут.

[OptionsSchemaName("http://something")]
public class MyWidget : WidgetBase
{
    public overide DoStuff(Options options)
    {
        //Do some FooWidget stuff
    }

    public static OptionSchema GetOptionsSchema()
    {
        //Return options for FooWidget
    }
}

Затем ваша инфраструктура обнаружения типов может искать неабстрактный IWidget и IWidget значимую ошибку прямо при запуске, так как the type MyWidget is lacking an OptionsSchemaName attribute. Every implementation of IWidget must define one. See http://mydocs for information the type MyWidget is lacking an OptionsSchemaName attribute. Every implementation of IWidget must define one. See http://mydocs for information the type MyWidget is lacking an OptionsSchemaName attribute. Every implementation of IWidget must define one. See http://mydocs for information.

Взрыв! Успешно справился!

1

В настоящее время невозможно применить атрибут во время компиляции; это было бы идеально для вашего случая использования. Также невозможно иметь abstract static метод или иметь статический метод, указанный в интерфейсе; поэтому нет способа убедиться, что метод действительно существует во время компиляции, за исключением принудительного применения метода экземпляра через абстрактный класс или интерфейс (который потребует доступа к экземпляру типа).

Я бы пошел с идеей атрибута - вполне разумно ожидать, что разработчики будут читать документацию; даже при переопределении абстрактного метода разработчику необходимо знать, как создать OptionSchema в переопределенном методе - назад к документации!

Ещё вопросы

Сообщество Overcoder
Наверх
Меню