有没有一种简单的方法在C#中模拟Objective-C的分类?

6
我遇到了一个奇怪的设计问题,这是我从未遇到过的...如果我使用Objective-C,我会用分类来解决它,但我必须使用C# 2.0。
首先,一些背景。在这个类库中,我有两个抽象层。底层实现了一个用于扫描内容的组件的插件架构(抱歉,不能更具体)。每个插件将以某种独特的方式进行扫描,但插件也可以根据其接受的内容类型而有所不同。由于各种原因,我不想通过插件接口公开泛型。因此,我最终得到了一个IScanner接口和一个派生接口,用于每种内容类型。
顶层是一个方便的包装器,接受包含各种部分的复合内容格式。不同的扫描程序将需要复合的不同部分,具体取决于他们感兴趣的内容类型。因此,我需要针对每个IScanner派生接口具有特定逻辑,以解析复合内容,查找所需的相关部分。
一种解决方法是简单地向IScanner添加另一个方法,并在每个插件中实现它。但是,两层设计的整个重点在于插件本身不需要知道复合格式。暴力解决方法是在上层进行类型测试和向下转换,但这些需要在未来添加对新内容类型的支持时仔细维护。在这种情况下,访问者模式也很尴尬,因为实际上只有一个访问者,但可访问类型的数量将随时间增加而增加(即-这些是访问者适用的相反条件)。此外,双重分派感觉像是过度设计,当我只想劫持IScanner的单一分派时!
如果我使用Objective-C,我会在每个IScanner派生接口上定义一个分类,并在那里添加parseContent方法。该类别将在上层中定义,因此插件不需要更改,同时避免了类型测试的需要。不幸的是,C#扩展方法无法工作,因为它们基本上是静态的(即-与调用站点使用的引用的编译时类型相关联,而不像Obj-C类别那样连接到动态分派)。更不用说,我必须使用C# 2.0,因此扩展方法甚至对我都不可用。:-P
那么,在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#中使用相同的技术,如果可以,怎么做呢?我不介意寻找替代方案,但公平起见,这不是我提出的问题。:)


你能指出一个好的资源去学习更多关于Objective-C分类的知识吗? - Fabrizio C.
http://en.wikipedia.org/wiki/Objective-C#Categories http://dotnetaddict.dotnetdevelopersjournal.com/orcas_langextend_vs_categories.htm http://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/Articles/chapter_6_section_1.html#//apple_ref/doc/uid/TP30001163-CH20-SW1 - Bruce Johnston
2个回答

1

听起来你的意思是你有一些内容布局类似于这样:

+--------+
| 部分1 |
| 类型A |
+--------+
| 部分2 |
| 类型C |
+--------+
| 部分3 |
| 类型F |
+--------+
| 部分4 |
| 类型D |
+--------+

并且你有每个部分类型的读取器。也就是说,AScanner 知道如何处理类型 A 的部分中的数据(例如上面的部分 1),BScanner 知道如何处理类型 B 的部分中的数据,依此类推。到目前为止我理解得对吗?

现在,如果我理解正确,你遇到的问题是类型读取器(IScanner 实现)不知道如何在组合容器中定位它们识别的部分。

你的组合容器能否正确枚举单独的部分(即它知道一个部分在哪里结束,另一个部分从哪里开始),如果可以,每个部分是否具有某种标识,扫描器或容器可以区分?

我的意思是,数据是否像这样布局?

+-------------+
| 部分 1      |
| 长度:100 |
| 类型:"A"   |
| 数据:...   |
+-------------+
| 部分 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. 听起来您可能需要一个代理类,它知道如何与容器和扫描仪通信。上述任何功能都可以在任何一个类中实现,只要容器已经公开足够的功能(或受保护的[这是一个词吗?]),以便外部/派生类能够访问相关数据。

从您在编辑后的问题中的伪代码看来,似乎您并没有真正从接口中获得任何好处,并且紧密地将插件耦合到主应用程序中,因为每种扫描器类型都有一个唯一的 IScanner 派生类,定义了唯一的“扫描”方法,CompositeScanner 类对于每个部分类型也有一个独特的“解析”方法。

我想说这是您的主要问题。 您需要将插件(我假设是 IScanner 接口的实现者)与主应用程序(我假设是 CompositeScanner 类所在的位置)分离。 我之前的一些建议就是我的实现方式,但确切的细节取决于您的 parseTypeX 函数是如何工作的。 这些内容可以被抽象化和概括吗?

假设您的parseTypeX函数与Composite类对象通信,以获取所需的数据。这些函数可否移至IScanner接口上的Parse方法中,并通过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);
        });
    }
}

当然,这可以通过删除 IScanner 上的单独 Parse 方法并直接将 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方法,那就更好了。

这种方法存在一些问题:1.扫描器不应该具有任何容器类型的知识。2.容器类型没有内置智能。它与C#中的纯旧数据一样接近。3.我无法更改容器类型;它是现有架构的一部分。 - Bruce Johnston
IPartProvider是一种可能性,尽管它还有其他我不想深入讨论的复杂性(这些复杂性是我的项目内部问题)。你提到的“代理类”就是我在原帖中提到的“上层”。这是必需的,因为扫描器和容器都是被动的。 - Bruce Johnston
你提到的“上层”是我最初所指的“IContainer”。也许,如果您展示一下您期望不同部分如何交互(在必要时使用虚假名称),那么更容易为您的模型提供适合的解决方案。 - P Daddy
你对IScanner派生接口的目的做出了错误的假设。它们代表不同类型内容的扫描仪,但对于每种内容类型,都会有几个独特的扫描器插件。此外,组合模式很丑陋,访问无法通用化,真是太可惜了。:-P - Bruce Johnston

1

我要试一试... ;-) 如果在您的系统中有一个阶段,当您填充您的“目录”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);
        }
    }
}

不要将其视为现成的代码:它只是用于解释目的。

编辑:回答Colonel Kernel的评论。 很高兴你觉得它有趣。 :-) 在这个简单的代码示例中,反射应该仅在字典初始化期间(或需要时)涉及,并且在此阶段您可以“强制”存在属性(甚至使用其他映射扫描仪和部件的方式)。我说“强制执行”,因为即使它不是编译时约束,我认为在将其投入生产之前,您将运行您的代码至少一次;-)所以如果需要,它可以是运行时约束。我会说灵感来自MEF或其他类似框架(非常轻微)。 只是我的两分钱。


我之前没有考虑过使用属性... 然而,这并没有解决问题。它只是将类型测试转换为基于反射的属性检查,并且它并不强制扫描器类包含该属性。不过这是一个有趣的方法。 - Bruce Johnston
请看我添加的最后一个“编辑”以获取对您评论的可能答案。 :-) - Fabrizio C.
在我的情况下,没有中央控制点来注册插件。它们的配置信息(包括完全限定类型名称)存储在数据库中,并按需加载。我无法更改这个。因此,属性检查对于我的目的来说会发生得太晚了。 - Bruce Johnston
如果配置在数据库中,这仍然可以接受(元数据,即属性驱动映射,只是其中之一选项),但我认为如果没有进一步的细节,我无法提供帮助,而且我不知道您是否想要透露它们...;-) - Fabrizio C.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接