【问题标题】:Designing an object with a long running constructor使用长时间运行的构造函数设计对象
【发布时间】:2013-01-26 02:00:31
【问题描述】:

我有一个类,旨在为在整个应用程序中多次使用的某些特定文件提供对某些元数据的快速访问。不幸的是,一些元数据只能通过非常长时间运行的方法调用来提取。

我有另一个类为长时间运行的方法提供异步包装器(可能是 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 回复。但是如果他们在返回后调用 if ,它将包含正确的信息。由于确实没有办法通知用户此方法已完成,我担心这会变成糟糕的用户体验,因为他们不必等待构造函数,而是必须等待所有属性设置。

【问题讨论】:

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

标签: c# .net design-patterns .net-3.5


【解决方案1】:

首先,您说无法通知用户获取Data2 已完成。这不是真的,您可以使用多种方式通知用户,例如事件或Task

但我认为你实际上应该重组你的班级。您说获取Data2 需要很长时间,这很可能意味着它使用了大量资源。因此,我认为您甚至不应该尝试初始化Data2,除非您必须这样做。你怎么知道的?用户将不得不告诉你。理想情况下,如果用户不想要Data2,他甚至不能访问它,这意味着将MetaData 分成两个类:类似于BasicMetaDataExtendedMetaData,它们继承自@987654329 @。

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)。但我喜欢你关于 Base 和 Extended 类的建议。由于我有一个已经从我的 MetaData 类继承的类,所以事情稍微复杂了一点,但我也可以将它拆分为 Base 和 Extended 类型。
【解决方案2】:

我认为您需要将注意力集中在以下模式上:延迟加载(仅在您真正需要时才调用'long'方法)和代理(如果需要实现缓存层,隐藏内部实现,可能有多种不同的对象底部的图案)。如果您决定使用多个对象来确保整体功能 - 那么 Facade 也可能是合理的选择。

【讨论】:

  • 我考虑过延迟加载,但正如我评论 DarthVader 的回答一样,由于我正在处理访问本地或网络驱动器上的文件,我宁愿在调用时处理任何 IO 异常构造函数,而不是当我第一次需要数据时,因为那时我的代码将处于更好的位置来处理异常。恐怕我对代理和外观模式了解不多,但我会研究它们,看看它们是否适合我的需求。
【解决方案3】:

这个问题你会得到几个不同的答案。这是我的看法。

IMO,您不应该在构造函数中调用任何操作,例如您现在正在执行的操作。你的 MetaData 构造函数中的所有东西都不应该从那里开始。

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

也看看这个问题: How much code should one put in a constructor?

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

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

您确实需要简化流程和设计。

【讨论】:

  • 另一方面,构造函数应该返回一个完全初始化的可用对象,而不是将来某个时候可能开始工作的东西。
  • 不!即:您可以为 DB ORM 设置一个类,并且出于任何原因,该类可能由于多种原因而失败。这就是为什么你有例外。但是构造函数不是做所有这些操作的地方。
  • 我认为构造函数正是初始化的正确位置,这就是它的用途。而且我不确定我是否遵循您的数据库示例,但如果课程将失败,我想尽快失败。而且我不认为要求该类的每个公共方法都调用类似 InitializeIfNecessary() 的东西(这是您的方法所要求的)是一个好主意。
  • 虽然我认为你的观点有很多优点,但我同意 svick 的初始化,尤其是对于这种情况。如果一个对象在构造函数返回后还没有准备好使用,那么它有什么用呢?由于此对象正在访问一个文件(可能位于本地或网络驱动器上),我宁愿在我能最好地处理异常时立即获取 IO 异常,而不是在代码块不知道的将来某个时间该怎么做。
猜你喜欢
  • 1970-01-01
  • 2014-02-13
  • 1970-01-01
  • 1970-01-01
  • 2018-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多