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

Bru*_*ton 6 c# oop design-patterns objective-c visitor

我有一个奇怪的设计情况,我以前从未遇到过......如果我使用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...
}
Run Code Online (Sandbox Code Playgroud)

编辑:为了澄清,我已经考虑过这个设计了很多.我有很多原因,其中大多数是我无法分享的,原因就在于它的原因.我还没有接受任何答案,因为虽然有趣,但他们回避了原来的问题.

事实是,在Obj-C中,我可以简单而优雅地解决这个问题.问题是,我可以在C#中使用相同的技术吗?如果是,如何使用?我不介意寻找替代方案,但公平地说这不是我问的问题.:)

P D*_*ddy 1

听起来你的意思是你的内容布局如下:

\n\n
\n+--------+\n| 第 1 部分 |\n| 类型 A |\n+--------+\n| 第 2 部分 |\n| 输入 C |\n+--------+\n| 第 3 部分 |\n| 输入 F |\n+--------+\n| 第 4 部分 |\n| 输入 D |\n+--------+\n
\n\n

并且每种零件类型都有读者。也就是说,AScanner知道如何处理类型A的部分(如上面的部分1)中的数据,BScanner知道如何处理类型B的部分数据,等等。到目前为止我是对的吗?

\n\n

现在,如果我理解您的意思,您遇到的问题是类型读取器(实现IScanner)不知道如何在复合容器中找到它们识别的部分。

\n\n

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

\n\n

我的意思是,数据是这样排列的吗?

\n\n
\n+-------------+\n| 第 1 部分 |\n| 长度: 100 |\n| 类型:“A”|\n| 数据:... |\n+-------------+\n| 第 2 部分 |\n| 长度:460 |\n| 类型:“C”|\n| 数据:... |\n+-------------+\n| 第 3 部分 |\n| 长度:26 |\n| 类型:“F”|\n| 数据:... |\n+-------------+\n| 第 4 部分 |\n| 长度:790 |\n| 类型:“D”|\n| 数据: ... |\n+-------------+\n
\n\n

如果您的数据布局与此类似,那么扫描仪是否无法请求容器的所有部件具有与给定模式匹配的标识符?就像是:

\n\n
class Container : IContainer{\n    IEnumerable IContainer.GetParts(string type){\n        foreach(IPart part in internalPartsList)\n            if(part.TypeID == type)\n                yield return part;\n    }\n}\n\nclass AScanner : IScanner{\n    void IScanner.ProcessContainer(IContainer c){\n        foreach(IPart part in c.GetParts("A"))\n            ProcessPart(part);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

或者,如果容器可能无法识别零件类型,但扫描仪能够识别其自己的零件类型,可能类似于:

\n\n
delegate void PartCallback(IPart part);\n\nclass Container : IContainer{\n    void IContainer.GetParts(PartCallback callback){\n        foreach(IPart part in internalPartsList)\n            callback(part);\n    }\n}\n\nclass AScanner : IScanner{\n    void IScanner.ProcessContainer(IContainer c){\n        c.GetParts(delegate(IPart part){\n            if(IsTypeA(part))\n                ProcessPart(part);\n        });\n    }\n\n    bool IsTypeA(IPart part){\n        // determine if part is of type A\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

也许我误解了您的内容和/或您的架构。如果是这样,请澄清,我会更新。

\n\n
\n\n

来自OP的评论:

\n\n
\n
    \n
  1. 扫描仪不应该了解集装箱类型。
  2. \n
  3. 容器类型没有内置智能。它与您在 C# 中获得的普通旧数据非常接近。
  4. \n
  5. 我无法更改容器类型;它是现有架构的一部分。
  6. \n
\n
\n\n

我的回复太长,无法发表评论:

\n\n
    \n
  1. 扫描仪必须有某种方法来检索它们处理的零件。如果您担心IScanner接口不应该意识到接口,IContainer以便您IContainer将来可以自由地更改接口,那么您可以通过以下几种方式之一进行妥协:

    \n\n
      \n
    • 您可以向扫描仪传递一个派生(或包含)IPartProvider的接口IContainer。这IPartProvider只会提供提供部件的功能,因此它应该非常稳定,并且可以在与 相同的程序集中定义IScanner,这样您的插件就不需要引用IContainer定义的程序集。
    • \n
    • 您可以将委托传递给扫描仪,让他们用来检索零件。这样,扫描器就不需要任何接口的知识(当然IScanner,除了代理)。
    • \n
  2. \n
  3. \n
  4. 听起来也许您需要一个知道如何与容器和扫描仪通信的代理类。上面提到的任何功能都可以在任何类中实现,只要容器已经公开(或受保护地[这是一个词吗?])公开足够的功能,外部/派生类将能够访问相关的功能。数据。
  5. \n
\n\n
\n\n

从您编辑的问题中的伪代码来看,您似乎并没有真正从界面中获得任何好处,并且将您的插件紧密耦合到您的主应用程序,因为每种扫描仪类型都有一个独特的派生,它定义了IScanner一个独特的“ scan”方法,并且该类CompositeScanner对于每个部件类型都有一个独特的“parse”方法。

\n\n

我想说这是你的首要问题。您需要将插件\xe2\x80\x94(我假设是IScanner接口\xe2\x80\x94)的实现者与主应用程序\xe2\x80\x94(我假设是该类所在的位置)解耦CompositeScanner。我之前的建议之一是如何实现它,但具体细节取决于您的parseTypeX函数如何工作。这些可以抽象和概括吗?

\n\n

据推测,您的parseTypeX函数与Composite类对象进行通信以获取它们所需的数据。难道不能将它们移动到通过类代理的接口Parse上的方法中以从对象获取此数据吗?像这样的东西:IScannerCompositeScannerComposite

\n\n
delegate byte[] GetDataHandler(int offset, int length);\n\ninterface IScanner{\n    void   Scan(byte[] data);\n    byte[] Parse(GetDataHandler getData);\n}\n\nclass Composite{\n    public byte[] GetData(int offset, int length){/*...*/}\n}\n\nclass CompositeScanner{}\n    IScanner realScanner;\n\n    public void ScanComposite(Composite c){\n        realScanner.Scan(realScanner.Parse(delegate(int offset, int length){\n            return c.GetData(offset, length);\n        });\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

当然,这可以通过删除单独的Parse方法IScannerGetDataHandler直接将委托传递给(如果需要,Scan其实现可以调用 private )来简化。Parse该代码看起来与我之前的示例非常相似。

\n\n

这种设计提供了我能想到的尽可能多的关注点分离和解耦。

\n\n
\n\n

我只是想到了其他一些你可能会觉得更容易接受的东西,而且确实可以提供更好的关注点分离。

\n\n

如果您可以让每个插件向应用程序“注册”,那么您可以将解析保留在应用程序中,只要插件可以告诉应用程序如何检索其数据即可。示例如下,但由于我不知道如何识别您的部件,因此我实现了两种可能性\xe2\x80\x94,一种用于索引部件,一种用于命名部件:

\n\n
// parts identified by their offset within the file\nclass MainApp{\n    struct BlockBounds{\n        public int offset;\n        public int length;\n\n        public BlockBounds(int offset, int length){\n            this.offset = offset;\n            this.length = length;\n        }\n    }\n\n    Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();\n\n    public void RegisterPlugin(Type type, int offset, int length){\n        plugins[type] = new BlockBounds(offset, length);\n    }\n\n    public void ScanContent(Container c){\n        foreach(KeyValuePair<Type, int> pair in plugins)\n            ((IScanner)Activator.CreateInstance(pair.Key)).Scan(\n                c.GetData(pair.Value.offset, pair.Value.length);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

或者

\n\n
// parts identified by name, block length stored within content (as in diagram above)\nclass MainApp{\n    Dictionary<string, Type> plugins = new Dictionary<string, Type>();\n\n    public void RegisterPlugin(Type type, string partID){\n        plugins[partID] = type;\n    }\n\n    public void ScanContent(Container c){\n        foreach(IPart part in c.GetParts()){\n            Type type;\n            if(plugins.TryGetValue(part.ID, out type))\n                ((IScanner)Activator.CreateInstance(type)).Scan(part.Data);\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

显然,我已经极大地简化了这些示例,但希望您能明白这一点。Activator.CreateInstance此外,如果您可以将工厂(或工厂委托)传递给该方法,而不是使用,那就太好了RegisterPlugin

\n