当代码依赖于两个对象的子类型时,是否有一个设计模式来处理 [英] Is there a design pattern to handle when code depends on the subtype of two objects

查看:108
本文介绍了当代码依赖于两个对象的子类型时,是否有一个设计模式来处理的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



我正在C#中工作,我会尝试尽可能地明确,以防我的问题有更好的解决方案。



我有一个报告模板,可以包含任何数量的功能打开。功能可能是信息表,饼图/条形图,列表等。我将生成报告作为文本文件或PDF(可能还有其他选项)。



到目前为止,我有一个 IFeature 接口,一些实现它的功能类型: ChartFeature ListFeature
我读取从数据库启用的功能列表,并将每个功能与数据id一起传递给一个方法,该方法返回一个填充的 IFeature 的正确类型。



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



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

  public void AddFeature(IFeature)
{
InsertSectionBreakIfNeeded();

if(IFeature is TableFeature)
{
TableFeature tf =(TableFeature)功能;
streamWriter.WriteLine(tf.Title); (int row = 0; row< tf.Data.First.Length; row ++)
{
for(int column = 0; i {
if(i!= 0)
{
streamWriter.Write(|);
}
streamWriter.Write(feature.Data [column] [row]);
}
}
}
else if(IFeature为ListFeature)
{
ListFeature lf =(ListFeature)功能;
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表格单元格,文本盒子等等。



这感觉很丑陋。我喜欢它有点更好,因为$ code> AddFeature(ListFeature){...} , AddFeature(ChartFeature)因为至少它是编译时间检查,但实际上它只是移动问题,所以现在外面,如果IReportWriter我打电话 if(功能是...)



将显示代码移动到功能中只会反转问题,因为它需要知道是否应该写纯文本或PDF。



任何建议,或者我最好只是使用我所拥有的,忽略我的感受?



编辑:
填写一些条件给人更好地了解发生了什么。不用担心这些例子中的确切代码,我只是把它从头顶写下来。

解决方案

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



处理这一点的一个标准模式称为访问者模式。它的描述追溯到原始的设计模式书,所以有很多的例子和分析它在那里。



基本的想法是,你有两个一般的东西 - 你有元素(这些是你正在处理的东西)和访问者,它们通过元素进行处理。你需要对这两个动态调度进行动态调度 - 所以调用的实际方法取决于元素和访问者的具体类型。



在C#中,你可以按照这个例子来定义一个IFeatureVisitor界面:

  public interface IFeatureVisitor {
void Visit (ChartFeature功能);
void访问(ListFeature功能);
// ...等一个功能类型
}

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

  public interface IFeature {
public void Accept(IFeatureVisitor游客);
}

您的功能实现将实现接受方法,如下所示:

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

然后你的报告编写者将实现IVisitor界面做任何它应该做的每一种类型。



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

  var writer = new HtmlReportWriter(); 
foreach(文档中的IFeature功能){
feature.Accept(writer);
}
writer.FinishUp();

这样做的方式是第一个接受的虚拟调用可以回到特定的具体类型。对访问方法的调用不是虚拟的 - 调用 visitor.Visit(this)调用正确的重载,因为在这一点上,它知道事物的确切静态类型被访问没有演员和类型的安全性被保留。



添加新的访问者类型时,这种模式是非常好的。当元素(您的案例中的功能)更改时,更加痛苦 - 每次添加新元素时,都需要更新IVisitor界面和所有实现。所以仔细考虑。



正如我所说,自出版以来已经有将近20年了,所以你可以在Visitor模式中找到很多分析和改进。有帮助,您可以开始继续分析。


I'll try to be as explicit as possible, in case there is a better solution for my problem than answering my question.

I'm working in C#.

I have a report template that can include any number of 'features' turned on. A feature might be a table of information, a pie/bar chart, a list, etc. I am generating the report as a text file, or a PDF (possibly other options in the future).

So far I have an IFeature interface, and some feature types implementing it: ChartFeature, ListFeature, etc. I read the list of features enabled from the database and pass each one to a method along with the data id and the method returns a populated IFeature of the proper type.

I also have an IReportWriter interface that TextReportWriter and PdfReportWriter implement. That interface has a method: AddFeature(IFeature).

The problem is that AddFeature in each writer ends up looking like:

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;
}

In the PDF writer the above would be modified to generate PDF table cells, text boxes, and so forth.

This feels ugly. I like it somewhat better as AddFeature(ListFeature){...}, AddFeature(ChartFeature) because at least then it's compile time checked, but in practice it just moves the problem so now outside if the IReportWriter I'm calling if(feature is ...).

Moving the display code into the feature just reverses the problem because it would need to know whether it should be writing plain text or a PDF.

Any suggestions, or am I best just using what I have and ignoring my feelings?

Edit: Filled in some of the conditions to give people a better idea of what is happening. Don't worry too much about the exact code in those examples, I just wrote it off the top of my head.

解决方案

The general case of your problem is called double-dispatch - you need to dispatch to a method based on the runtime type of two parameters, not just one (the "this" pointer).

One standard pattern to deal with this is called the Visitor pattern. It's description traces back to the original Design Patterns book, so there's lots of example and analysis of it out there.

The basic idea is that you have two general things - you have the Elements (which are the things that you're processing) and Visitors, which process over the Elements. You need to do dynamic dispatch over both of them - so the actual method called varies depending on both the concrete type of the element and of the visitor.

In C#, and kinda sorta following your example, you'd define an IFeatureVisitor interface like this:

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

Then, in your IFeature interface, add an "Accept" method.

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

Your feature implementations would implement the Accept method like so:

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

And then your report writers would implement the IVisitor interface and do whatever it's supposed to do in each type.

To use this, it's look something like this:

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

The way this works is that the first virtual call to Accept resolves back to the concrete type of the feature. The call to the Visit method is NOT virtual - the call to visitor.Visit(this) calls the correct overload since at that point it knows the exact static type of the thing that's being visited. No casts and type safety is preserved.

This pattern is great when new visitor types get added. It's much more painful when the elements (features in your case) change - every time you add a new element, you need to update the IVisitor interface and all the implementations. So consider carefully.

As I mentioned, there's been almost 20 years since the book was published, so you can find lots of analysis and improvements on Visitor pattern out there. Helpfully this gives you enough of a start to continue your analysis.

这篇关于当代码依赖于两个对象的子类型时,是否有一个设计模式来处理的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆