设计一个具有长时间运行构造函数的对象。

3
我有一个类,旨在为应用程序中多次使用的某些特定文件提供快速访问其元数据。不幸的是,其中一些元数据只能通过非常耗时的方法调用来提取。
我有另一个类,为长时间运行的方法提供了异步包装器(根据文件大小可能需要5分钟或更长时间),但我正在尝试弄清楚如何调用此异步方法,以及是否适合将其放入构造函数中,或者是否存在更好的设计模式来处理这种情况。
以下是一些伪代码,以尝试说明我的问题:
public class MetaData
{
    public string Data1 { get; private set; }
    public string Data2 { get; private set; }

    public MetaData(String filepath)
    {
        var extractor = new ExtractMetaData(filepath);  //create instance of class that fetches the metadata

        this.Data1 = extractor.GetData1(); // short running method

        extractor.Data2Received += Data2Received;  
        extractor.GetData2Async();  // long running method, called with via async method

    }        

    private void Data2Received(object sender, MetaDataEventArgs args)
    {
        this.Data2 = args.Data;  // finally set Data2 property
    }
}

class ExtractMetaData
{

    public event Data2ReceivedEventHandler Data2Received;

    public ExtractMetaData (string filePath) { }

    public String GetData1();  // very fast method to get Data1
    public void GetData2Async();  // very slow method to get Data2

}

我在尝试弄清楚是否有更好的方法来完成这个过程?
目前我的代码几乎不需要等待构建MetaData,但如果有人在GetData2Async()方法返回并触发Data2Received事件之前就尝试访问MetaData.Data2属性,则会收到null的响应。但是如果他们在返回后调用它,它将包含正确的信息。由于没有真正的方法通知用户此方法已完成,我担心这将成为糟糕的用户体验,因为他们不必等待构造函数,但必须等待所有属性被设置。

我认为这取决于上下文,即您正在提供什么类型的API,但我可能会将调用以异步回调形式或阻塞标准调用的形式提供。我不认为在这种情况下包装器会有任何收益,除非您能够返回部分答案,否则我认为您无法真正设计出一条出路。这只是我的个人意见,但我会将选择留给调用者,并专注于优化该例程。 - Tim
@Tim,这个包装器实际上是我为其他目的编写的一个独立库的一部分,恰好提供异步功能以避免阻塞。如果只是10或15秒的问题,我可能会选择阻塞方法,但当我意识到可能需要5分钟时,我开始考虑另一种方法(在构造函数中调用异步方法)。 - psubsee2003
抱歉,我想我想说的是,我认为你不能对包装器应用任何技巧来提高客户端的清晰度或性能(除非部分答案有一定的用途)。包装器本身仍然允许您将“这个方法很慢,所以我需要封装它”的逻辑与解析逻辑分离,因此我相信这是值得的。 - Tim
@Tim 关于优化这个程序,那是不可能的...实际上长时间运行的方法是一个外部库,我无法控制。我的选择只有两个,要么放弃长时间运行方法获取的数据,要么接受长时间执行。我选择了后者,因为额外的数据对我的API的某些部分非常有用。 - psubsee2003
3个回答

2
首先,你说没有办法通知用户获取 Data2 的完成情况。这不是真的,你可以使用任何一种方式来通知用户,例如事件或Task
但我认为你应该重新构造你的类。你说获取 Data2 非常耗时,这很可能意味着它使用了大量资源。因此,除非必须,否则我认为你甚至不应该尝试初始化 Data2。那么你如何知道呢?用户必须告诉你。理想情况下,如果用户不想要 Data2,他甚至不应该能够访问它,这意味着将 MetaData 拆分成两个类:像 BasicMetaData 和继承自 BasicMetaDataExtendedMetaData
ExtendedMetaData 中,你可以有一些方法来通知用户初始化完成(最有可能使用事件),或者你可以使构造函数等待初始化完成(你可以使用 Monitor.Wait()Monitor.Pulse() 来实现)。
个人认为,最好的选择是有一个静态工厂方法,它将返回 Task<ExtendedMetaData>。这样,用户可以同步等待结果(使用 Result)或异步等待结果(使用 ContinueWith() 或在可用的情况下使用 await)。这在 .Net 4.5 中特别有用(因为有 await),但在 .Net 4.0 上也是如此。不幸的是,你的问题标签表明你正在使用 .Net 3.5,它没有 Task。如果可能的话,我建议你升级。

我实际上正在使用 .Net 3.5 是为了其他原因(主要是与那些固定在 .Net 3.5 上的应用程序保持向后兼容性,否则我会选择 .Net 4)。但我喜欢你关于基类和扩展类的建议。这稍微复杂了一些,因为我已经有一个继承自我的“MetaData”类的类,但我也可以将其拆分成基类和扩展类型。 - psubsee2003

1
我认为你需要关注以下模式:懒加载(仅在实际需要时调用“长”方法)和代理(如果需要实现缓存层、隐藏内部实现、底层可能有多种不同的模式,则可以使用代理)。如果你决定使用多个对象来确保整个功能-那么外观模式也是一个合理的选择。

我考虑过懒加载,但是正如我在DarthVader的回答中所评论的那样,由于我正在处理访问本地或网络驱动器上的文件,因此我宁愿在调用构造函数时处理任何IO异常,而不是在我首次需要数据时处理异常,因为这时我的代码将处于更好的位置来处理异常。 我不太清楚代理和外观模式,但我会研究一下它们,看看它们是否适合我的需求。 - psubsee2003

0

对于这个问题,你会得到几个不同的答案。以下是我的看法。

在我看来,你不应该在构造函数中调用任何操作,比如你现在正在做的事情。你的MetaData构造函数中所有那些东西本来就不应该存在。

当你实例化一个对象时,它可能会抛出异常,这很正常,但你的对象不会被构建。一些最佳实践是,构造函数应该运行时间短,并确保在构造函数之后创建对象图。

也可以看看这个问题: 一个构造函数应该放多少代码?

或者,你应该注入你的依赖项并创建方法来填充数据。

如果你能描述一下你的问题,那将更有帮助。

你真的需要简化你的过程和设计。


另一方面,构造函数应该返回一个完全初始化可用的对象,而不是可能在未来开始工作的东西。 - svick
你可以为数据库ORM创建一个类,但由于许多原因,该类可能会失败。这就是为什么有异常的原因。但构造函数绝不是执行所有这些操作的地方。 - DarthVader
我认为构造函数是初始化的恰当位置,那就是它存在的原因。我不确定我是否听懂了你的数据库示例,但如果类将失败,我希望尽早失败。我不认为要求该类的每个公共方法都调用诸如InitializeIfNecessary()之类的东西(这是你的方法所需的)是一个好主意。 - svick
虽然我认为你的观点很有价值,但在这种情况下,我同意svick的初始化方式。如果构造函数返回后对象还没有准备好使用,那么它有什么用呢?由于该对象正在访问文件(可能位于本地或网络驱动器上),因此我宁愿在最佳处理异常的时候立即获取IO异常,而不是在未来某个时间代码块对此一无所知时再获取异常。 - psubsee2003

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