Есть ли простой способ эмулировать категории Objective-C в C#?

У меня странная ситуация с дизайном, с которой я никогда не сталкивался раньше... Если бы я использовал Objective-C, я бы решал ее с помощью категорий, но я должен использовать C# 2.0.

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

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

Один из способов решить эту проблему - просто добавить другой метод в IScanner и внедрить его в каждый плагин. Однако весь смысл двухслойного дизайна заключается в том, что самим плагинам не нужно знать о составном формате. Грубым способом решения этой проблемы является использование типовых тестов и даункастов на верхнем уровне, но их необходимо тщательно поддерживать, так как в будущем будет добавлена ​​поддержка новых типов контента. Шаблон "Посетитель" также будет неудобным в этой ситуации, поскольку в действительности существует только один Посетитель, но число различных типов "Видимых" будет только увеличиваться со временем (т. Е. Это противоположные условия, для которых подходит Посетитель). Кроме того, двойная отправка кажется излишней, когда на самом деле все, что я хочу, - это захватить единственную отправку IScanner!

Если бы я использовал Objective-C, я бы просто определил категорию на каждом интерфейсе, производном от IScanner, и добавил туда метод parseContent. Категория будет определена на верхнем уровне, поэтому плагины не нужно будет менять, одновременно избегая необходимости типовых тестов. К сожалению, методы расширения C# не будут работать, потому что они в основном статичны (т. Е. Связаны с типом времени компиляции ссылки, используемой на сайте вызова, а не подключены к динамической диспетчеризации, такой как Obj-C Categories). Не говоря уже о том, что я должен использовать C# 2.0, поэтому методы расширения мне даже не доступны.:-П

Так есть ли простой и простой способ решения этой проблемы в C#, сродни тому, как ее можно решить с помощью категорий Objective-C?


РЕДАКТИРОВАТЬ: Некоторые псевдокод, чтобы помочь понять структуру текущего проекта:

interface IScanner
{ // Nothing to see here...
}

interface IContentTypeAScanner : IScanner
{
    void ScanTypeA(TypeA content);
}

interface IContentTypeBScanner : IScanner
{
    void ScanTypeB(TypeB content);
}

class CompositeScanner
{
    private readonly IScanner realScanner;

    // C-tor omitted for brevity... It takes an IScanner that was created
    // from an assembly-qualified type name using dynamic type loading.

    // NOTE: Composite is defined outside my code and completely outside my control.
    public void ScanComposite(Composite c)
    {
        // Solution I would like (imaginary syntax borrowed from Obj-C):
        // [realScanner parseAndScanContentFrom: c];
        // where parseAndScanContentFrom: is defined in a category for each
        // interface derived from IScanner.

        // Solution I am stuck with for now:
        if (realScanner is IContentTypeAScanner)
        {
            (realScanner as IContentTypeAScanner).ScanTypeA(this.parseTypeA(c));
        }
        else if (realScanner is IContentTypeBScanner)
        {
            (realScanner as IContentTypeBScanner).ScanTypeB(this.parseTypeB(c));
        }
        else
        {
            throw new SomeKindOfException();
        }
    }

    // Private parsing methods omitted for brevity...
}

РЕДАКТИРОВАТЬ: Чтобы уточнить, я уже много думал об этом проекте. У меня есть много причин, большинство из которых я не могу объяснить, почему так оно и есть. Я еще не принял никаких ответов, потому что, хотя они и интересны, они уклоняются от первоначального вопроса.

Дело в том, что в Obj-C я мог бы решить эту проблему просто и элегантно. Вопрос в том, могу ли я использовать ту же технику в C#, и если да, то как? Я не против поиска альтернатив, но, честно говоря, это не тот вопрос, который я задал.:)

2 ответа

Похоже, вы говорите, что у вас есть контент, выложенный примерно так:

+ -------- +
| часть 1 |
| Тип А |
+--------+
| часть 2 |
| тип C |
+--------+
| часть 3 |
| тип F |
+--------+
| часть 4 |
| Тип D |
+--------+

и у вас есть читатели для каждого типа детали. То есть AScanner знает, как обрабатывать данные в части типа A (например, в части 1 выше), BScanner знает, как обрабатывать данные в части типа B, и так далее. Я прав до сих пор?

Теперь, если я вас понимаю, проблема в том, что читатели типа (IScanner реализации) не знают, как найти части (ы), которые они распознают в вашем составном контейнере.

Может ли ваш составной контейнер правильно перечислить отдельные части (т. Е. Знает ли он, где заканчивается одна часть, а другая начинается), и, если да, имеет ли каждая часть какую-то идентификацию, которую может различить сканер или контейнер?

Я имею в виду, что данные выложены примерно так?

+ ------------- +
| часть 1 |
| длина: 100 |
| Тип: "А"   |
| данные: ...   |
+-------------+
| часть 2 |
| длина: 460 |
| Тип: "C"   |
| данные: ...   |
+-------------+
| часть 3 |
| длина: 26  |
| Тип: "F"   |
| данные: ...   |
+-------------+
| часть 4 |
| длина: 790 |
| тип: "D"   |
| данные:... |
+ ------------- +

Если ваш макет данных похож на это, могут ли сканеры не запрашивать у контейнера все части с идентификатором, соответствующим заданному шаблону? Что-то вроде:

class Container : IContainer{
    IEnumerable IContainer.GetParts(string type){
        foreach(IPart part in internalPartsList)
            if(part.TypeID == type)
                yield return part;
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        foreach(IPart part in c.GetParts("A"))
            ProcessPart(part);
    }
}

Или, если, возможно, контейнер не сможет распознать тип детали, но сканер сможет распознать свой собственный тип детали, возможно, что-то вроде:

delegate void PartCallback(IPart part);

class Container : IContainer{
    void IContainer.GetParts(PartCallback callback){
        foreach(IPart part in internalPartsList)
            callback(part);
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        c.GetParts(delegate(IPart part){
            if(IsTypeA(part))
                ProcessPart(part);
        });
    }

    bool IsTypeA(IPart part){
        // determine if part is of type A
    }
}

Возможно, я неправильно понял ваш контент и / или вашу архитектуру. Если так, уточните пожалуйста, а я обновлю.


Комментарий от ОП:

  1. Сканеры не должны знать тип контейнера.
  2. Тип контейнера не имеет встроенного интеллекта. Это так близко к простым старым данным, как вы можете получить в C#.
  3. Я не могу изменить тип контейнера; Это часть существующей архитектуры.

Мои ответы слишком длинные для комментариев:

  1. У сканеров должен быть какой-то способ извлечения части, которую они обрабатывают. Если вы обеспокоены тем, что IScanner интерфейс не должен знать о IContainer интерфейс, так что у вас есть свобода изменять IContainer Интерфейс в будущем, то вы можете пойти на компромисс одним из нескольких способов:

    • Вы можете передать сканеры IPartProvider интерфейс, который IContainer получено из (или содержится). это IPartProvider будет обеспечивать только функциональность обработки деталей, поэтому он должен быть довольно стабильным, и его можно определить в той же сборке, что и IScanner, так что ваши плагины не должны ссылаться на сборку, где IContainer был определен.
    • Вы можете передать делегат сканерам, которые они могут использовать для извлечения деталей. Тогда сканерам не понадобится знание какого-либо интерфейса (кроме IScanner, конечно), только делегата.
  2. а также

  3. Возможно, вам нужен суррогатный класс, который знает, как взаимодействовать как с контейнером, так и со сканерами. Любая из упомянутых выше функциональных возможностей может быть реализована в любом старом классе, если контейнер уже предоставляет достаточно функциональных возможностей публично (или защищенно [это слово?]), Что внешний / производный класс сможет получить доступ к соответствующим данным,

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

Я бы сказал, что это ваша основная проблема. Вы должны отделить плагины, которые, как я полагаю, являются разработчиками IScanner интерфейс - из основного приложения - который я предполагаю, где CompositeScanner класс живет. Одно из моих ранних предложений заключается в том, как мне это реализовать, но точные детали зависят от того, как parseTypeХ функции работают. Могут ли они быть обобщены и обобщены?

Предположительно, ваш parseTypeХ функции связываются с Composite Объект класса, чтобы получить данные, которые им нужны. Могут ли они быть перенесены в Parse метод на IScanner интерфейс, который прокси через CompositeScanner класс, чтобы получить эти данные из Composite объект? Что-то вроде этого:

delegate byte[] GetDataHandler(int offset, int length);

interface IScanner{
    void   Scan(byte[] data);
    byte[] Parse(GetDataHandler getData);
}

class Composite{
    public byte[] GetData(int offset, int length){/*...*/}
}

class CompositeScanner{}
    IScanner realScanner;

    public void ScanComposite(Composite c){
        realScanner.Scan(realScanner.Parse(delegate(int offset, int length){
            return c.GetData(offset, length);
        });
    }
}

Конечно, это можно упростить, удалив отдельные Parse метод на IScanner и просто передавая GetDataHandler делегировать непосредственно Scan (чья реализация могла бы назвать частную Parseпри желании). Код выглядит очень похоже на мои предыдущие примеры.

Этот дизайн обеспечивает настолько большое разделение проблем и разделение, насколько я могу себе представить.


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

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

// parts identified by their offset within the file
class MainApp{
    struct BlockBounds{
        public int offset;
        public int length;

        public BlockBounds(int offset, int length){
            this.offset = offset;
            this.length = length;
        }
    }

    Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();

    public void RegisterPlugin(Type type, int offset, int length){
        plugins[type] = new BlockBounds(offset, length);
    }

    public void ScanContent(Container c){
        foreach(KeyValuePair<Type, int> pair in plugins)
            ((IScanner)Activator.CreateInstance(pair.Key)).Scan(
                c.GetData(pair.Value.offset, pair.Value.length);
    }
}

или же

// parts identified by name, block length stored within content (as in diagram above)
class MainApp{
    Dictionary<string, Type> plugins = new Dictionary<string, Type>();

    public void RegisterPlugin(Type type, string partID){
        plugins[partID] = type;
    }

    public void ScanContent(Container c){
        foreach(IPart part in c.GetParts()){
            Type type;
            if(plugins.TryGetValue(part.ID, out type))
                ((IScanner)Activator.CreateInstance(type)).Scan(part.Data);
        }
    }
}

Очевидно, я чрезвычайно упростил эти примеры, но, надеюсь, вы поняли идею. Кроме того, вместо использования Activator.CreateInstanceбыло бы хорошо, если бы вы могли передать фабрику (или делегата фабрики) RegisterPlugin метод.

Я собираюсь попробовать...;-) Если в вашей системе есть этап, когда вы заполняете свой "каталог" IScanner предметы, которые вы могли бы подумать о украшении вашего IScannerс атрибутом, который указывает, что Part они заинтересованы. Затем вы можете отобразить эту информацию и запустить сканирование вашего Composite с картой. Это не полный ответ: если у меня будет немного времени, я попытаюсь уточнить...

Изменить: немного псевдокода, чтобы поддержать мое запутанное объяснение

public interface IScanner
{
    void Scan(IPart part);
}

public interface IPart
{
    string ID { get; }
}

[ScannedPart("your-id-for-A")]
public class AlphaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

[ScannedPart("your-id-for-B")]
public class BetaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

public interface IComposite
{
    List<IPart> Parts { get; }
}

public class ScannerDriver
{
    public Dictionary<string, IScanner> Scanners { get; private set; }

    public void DoScan(IComposite composite)
    {
        foreach (IPart part in composite.Parts)
        {
            IScanner scanner = Scanners[part.ID];
            scanner.Scan(part);
        }
    }
}

Не принимайте это как есть: это для целей объяснения.

Изменить: ответ на комментарии полковника Кернел. Я рад, что вам интересно.:-) В этом простом наброске отражения кода следует участвовать непосредственно во время инициализации Словаря (или, когда это необходимо), и на этом этапе вы можете "навязать" наличие атрибута (или даже использовать другие способы отображения сканеров и деталей). Я говорю "принудительно", потому что, даже если это не ограничение времени компиляции, я думаю, что вы запустите свой код хотя бы один раз, прежде чем вводить его в производство;-), так что это может быть ограничение времени выполнения, если это необходимо. Я бы сказал, что вдохновение - это нечто очень легкое, похожее на MEF или другие подобные структуры. Просто мои 2 цента.

Другие вопросы по тегам