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# 中使用相同的技术吗?如果可以,如何使用?我不介意寻找替代品,但公平地说,这不是我问的问题。:)

4

2 回答 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
    }
}

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


来自OP的评论:

  1. 扫描仪不应该对容器类型有任何了解。
  2. 容器类型没有内置智能。它与您在 C# 中获得的普通旧数据一样接近。
  3. 我无法更改容器类型;它是现有架构的一部分。

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

  1. 扫描仪必须有某种方式来检索他们处理的零件。如果您担心IScanner界面不应该知道IContainer界面,以便您IContainer将来可以自由更改界面,那么您可以通过以下几种方式之一进行妥协:

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

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

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

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

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其实现可以调用 private )来简化。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

于 2009-01-07T17:35:51.820 回答
1

我将尝试... ;-) 如果在您的系统中有一个阶段,当您填充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美分。

于 2009-01-07T17:44:47.170 回答