Есть ли простой способ эмулировать категории Objective C в C #? - PullRequest
6 голосов
/ 07 января 2009

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

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

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

Один из способов решить эту проблему - просто добавить другой метод в 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 голос
/ 07 января 2009

Я собираюсь попробовать ... ;-) Если в вашей системе есть этап, когда вы заполняете свой «каталог» IScanner объектов, вы можете подумать о том, чтобы украсить свои IScanner s атрибутом, указывающим, в каком 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 цента.

1 голос
/ 07 января 2009

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

+--------+
| part 1 |
| type A |
+--------+
| part 2 |
| type C |
+--------+
| part 3 |
| type F |
+--------+
| part 4 |
| type D |
+--------+

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

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

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

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

+-------------+
| part 1      |
| length: 100 |
| type: "A"   |
| data: ...   |
+-------------+
| part 2      |
| length: 460 |
| type: "C"   |
| data: ...   |
+-------------+
| part 3      |
| length: 26  |
| type: "F"   |
| data: ...   |
+-------------+
| part 4      |
| length: 790 |
| type: "D"   |
| data: ...   |
+-------------+

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

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 который определяет уникальный метод "scan", а класс CompositeScanner имеет уникальный метод "parse" для каждого типа детали.

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

Предположительно, ваши функции parseType X связываются с объектом класса 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.

...