当代码依赖于两个对象的子类型时,是否有设计模式来处理

Is there a design pattern to handle when code depends on the subtype of two objects

我会尽可能明确,以防我的问题有比回答我的问题更好的解决方案。

我在 C# 工作。

我有一个报告模板,可以包含任意数量的 'features' 打开。功能可能是 table 信息、pie/bar 图表、列表等。我将报告生成为文本文件或 PDF(将来可能有其他选项)。

到目前为止,我有一个 IFeature 接口,以及实现它的一些功能类型:ChartFeatureListFeature 等。 我从数据库中读取了启用的功能列表,并将每个功能连同数据 ID 和方法 returns 传递给一个方法 IFeature 正确类型。

我还有一个 IReportWriter 接口,由 TextReportWriter 和 PdfReportWriter 实现。该接口有一个方法:AddFeature(IFeature).

问题是每个作者的 AddFeature 最终看起来像:

public void AddFeature(IFeature)
{
    InsertSectionBreakIfNeeded();

    if(IFeature is TableFeature)
    {
        TableFeature tf = (TableFeature)feature;
        streamWriter.WriteLine(tf.Title);
        for(int row=0; row < tf.Data.First.Length; row++)
        {
            for(int column=0; i < tf.Data.Length; i++)
            {
                if(i != 0)
                {
                    streamWriter.Write("|");
                }
                streamWriter.Write(feature.Data[column][row]);
            }
        }
    }
    else if(IFeature is ListFeature)
    {
        ListFeature lf = (ListFeature)feature;
        streamWriter.Write(lf.Title + ": ");
        bool first = true;
        foreach(var v in lf.Data)
        {
            if(!first)
            {
                streamWriter.Write(", ");
            }
            else
            {
                first = false;
            }
            streamWriter.Write(v);
        }
    }
    ...
    else
    {
        throw new NotImplementedException();
    }
    sectionBreakNeeded = true;
}

在 PDF 编写器中,上述内容将被修改以生成 PDF table 单元格、文本框等。

这感觉很难看。我更喜欢 AddFeature(ListFeature){...}AddFeature(ChartFeature),因为至少那时它会检查编译时间,但实际上它只是将问题转移到外面,如果我正在调用 if(feature is ...) 的 IReportWriter。

将显示代码移到该功能中正好扭转了问题,因为它需要知道它应该编写纯文本还是 PDF。

有什么建议,还是我最好只用我所拥有的而忽略我的感受?

编辑: 填写一些条件可以让人们更好地了解正在发生的事情。不用太在意那些例子中的确切代码,我只是随手写下的。

我想您正在尝试绘制一些东西(即将其输出为 pdf 或文本或其他...)。

我的猜测是这样的:

interface IReportWriter {
    void AddFeature(IFeature feature);
    // Some other method to generate the output.
    IOutput Render();
    // Drawing primitives that every report writer implements
    void PrintChar(char c);
    void DrawLine(Point begin, Point end);
    ...
}

// Default implementation for ReportWriters
abstract class AbstractReportWriter {
    private IList<IFeature> features = new List<IFeature>();

    ...

    public void AddFeature(IFeature feature) {
        this.features.Add(feature);
    }

    public IOutput Render() {
        foreach(var feature in this.features) {
            feature.RenderOn(this);
        }
    }

    // Leave the primitives abstract
    public abstract void PrintChar(char c);
    public abstract void DrawLine(Point begin, Point end);
}

在功能方面:

interface IFeature {
    void RenderOn(IReportWriter);
}

下面是 ChartFeature 的示例实现:

public class ChartFeature : IFeature {
    ...
    public void RenderOn(IReportWriter report) {
       // Draw the chart based on the primitives.
       report.DrawLine(..., ...);
       ...
    }
}

我会以稍微不同的方式构建它:

我希望有一个 IReport 对象组成报表中的所有特征。该对象将具有方法 AddFeature(IFeature)GenerateReport(IReportWriter)

然后我会让 IFeature 实施 WriteFeature(IReport, IReportWriter) 并且通过这种方式将功能的实际处理方式委托给功能本身。

您构建代码的方式让我认为没有办法以格式不可知的方式编写可由任何给定编写器处理的功能,因此让对象本身处理该问题。

你的问题的一般情况称为双分派 - 你需要根据两个参数的运行时类型分派到一个方法,而不仅仅是一个("this" 指针)。

处理此问题的一种标准模式称为访问者模式。它的描述可以追溯到最初的设计模式书,所以那里有很多例子和分析。

基本思想是您有两个通用的东西 - 您有元素(这是您正在处理的东西)和访问者,它们处理元素。您需要对它们进行动态分派 - 因此实际调用的方法因元素和访问者的具体类型而异。

在 C# 中,有点像您的示例,您将定义一个 IFeatureVisitor 接口,如下所示:

public interface IFeatureVisitor {
    void Visit(ChartFeature feature);
    void Visit(ListFeature feature);
    // ... etc one per type of feature
}

然后,在您的 IFeature 界面中,添加一个 "Accept" 方法。

public interface IFeature {
    public void Accept(IFeatureVisitor visitor);
}

您的功能实现将像这样实现 Accept 方法:

public class ChartFeature : IFeature {
    public void Accept(IFeatureVisitor visitor) {
        visitor.Visit(this);
    }
}

然后您的报告编写者将实现 IVisitor 接口并在每种类型中执行它应该执行的任何操作。

要使用它,它看起来像这样:

var writer = new HtmlReportWriter();
foreach(IFeature feature in document) {
    feature.Accept(writer);
}
writer.FinishUp();

其工作方式是对 Accept 的第一个虚拟调用解析回功能的具体类型。对 Visit 方法的调用不是虚拟的 - 对 visitor.Visit(this) 的调用调用了正确的重载,因为此时它知道正在访问的事物的确切静态类型。不保留类型转换和类型安全。

当添加新的访问者类型时,这种模式非常有用。当元素(在您的情况下为特征)发生变化时会更加痛苦 - 每次添加新元素时,您都需要更新 IVisitor 接口和所有实现。所以慎重考虑。

正如我提到的,这本书出版已经快 20 年了,所以您可以在那里找到很多关于访问者模式的分析和改进。有益的是,这为您提供了足够的起点来继续您的分析。

我会避免使用 Visitor,原因有二:1) 它很复杂,并且 2) 您的 IFeatureIReportWriter 层次结构似乎都可以扩展。 Visitor 仅在访问的 Element 层次结构稳定时才有效。请参阅 中@Will 的评论。简单也是一种好设计。

这是您的代码在 UML class 图中的样子:

AddFeature 似乎是一个不一致的名字。这个方法所做的是格式化输出,所以我会适当地命名它。

如果您遵循 Replace conditional with polymorphism 重构,您可以添加每个具体功能将实现的 IFeature.WriteOutput() 方法。那么你在 IReportWriter 中的调用看起来像

public void AddFeature(IFeature feature)
{
    InsertSectionBreakIfNeeded();
    feature.WriteOutput();
    sectionBreakNeeded = true;
}

从某种意义上说,您只对代码应用了策略模式,其中 IFeature 扮演 Strategy 的角色,而 IReportWriter 扮演上下文的角色:


为抽象工厂编辑

它看起来不太简单,但您的示例代码并没有真正考虑 [PDF, Text] 和 [Chart, List] 的所有排列。

我建议使用 PdfReportFeature 和 ListReportFeature 的摘要 classes/interfaces,以防有一些功能,例如创建格式序言。如果需要,您可以应用模板方法模式。

这个想法是每个具体的 class,例如 PdfListFeature,都会有自己的 WriteOutput 方法来完成它需要做的事情。具体的 ReportWriter 只是调用 feature.WriteOutput() 为任何注入(聚合)到报告中的特征。

没有 double-dispatch 因为您不会将 PDF 和文本报告混合在一起(访客对我来说真的没有意义)。当您创建报告时,它是一种或另一种类型。您的抽象工厂模式将帮助您创建图表或列表的正确 class 并将其传递给编写器。

我更新了上面的策略部分以与抽象工厂方法保持一致。我希望这是有道理的。