如何在MVVM中组合异步和非异步服务?

时间:2017-07-07 04:20:05

标签: c# mvvm async-await mvvm-light

方案

在基于MVVMLight的应用程序中,我使用一些视图和服务来进行一些工作时间计算。其中有以下服务......

  • DataService :顾名思义,用于从数据库中获取数据。因为它是每个性质的I / O操作,所以方法使用async / await。
  • 合同:这是一个CPU绑定服务,实际计算工作时间(例如,给定的一天,一周,一个月......)

现在,让我们看一下以下方法(位于Contract服务中):

public TimeSpan GetWorkTimeForDay(Date date)
{
    // Check if that result was already calculated before and is still
    // valid. In this case, load it and return it
    if (ContractCalculationCacheResults.Contains(date))
        return ContractCalculationCacheResults.Get(date);

    var finalWorkTime = TimeSpan.Zero;

    // Get the work day from the database in a synchronous fashion
    var workDay = DataService.GetWorkDay(date);

    // if there is a work day in the database then finally calculate the work time
    if (workDay != null)
        finalWorkTime = GetWorkTimeForDay(workDay);

    // cache the result
    ContractCalculationCacheResults.Put(date, finalWorkTime);

    // return the final result
    return finalWorkTime;
}

此方法位于Contract服务中。它依赖于DataService来获取计算所需的数据。

如您所见,DataService.GetWorkDay()目前是同步的。这就是数据库访问一段时间的方式。现在,在将应用程序从WP8 Silverlight移植到UWP时,我想清理代码并更改数据服务,以便它只提供async方法而不是同步方法。但是现在我正在努力将两个实际上独立的服务结合起来,其中一个是同步的,而另一个是同步的。

问题

我需要更新方法GetWorkTimeForDay()(以及其他许多看起来相似的方法)以使用新的DataService.GetWorkDayAsync()方法,该方法的名称定义如下:

public async Task<WorkDay> GetWorkDayAsync(Date dateKey)

但是,这导致许多未解决的问题,因为突然GetWorkTimeForDay也应该成为async方法,即使它的性质受CPU限制。所以这没有意义。

可能的解决方案

1。使GetWorkTimeForDay成为异步方法

结果将是这样的:

public async Task<TimeSpan> GetWorkTimeForDay(Date date)
{
    ...

    // Get the work day from the database in a synchronous fashion
    var workDay = await DataService.GetWorkDayAsync(date);

    ...
}

虽然这看起来很简单,但它对其余代码有很多影响。仅仅因为在调用GetWorkTimeForDay的任何地方我都必须更改代码,因为此方法现在是async。我必须await,因此可能需要更改调用方法......等等。但有时不需要异步调用GetWorkTimeForDay()因为它已经在单独的线程或后台任务中运行。更不用说目前存在的所有单元测试。因此,有些情况下它不会阻止UI。

此外,突然CPU绑定计算变得异步,而不是它应该如何工作(至少如果我正确地理解了Stephen Cleary的this帖子(和其他人)。

2。分开服务

因此,我想到的下一个解决方案是分离服务。换句话说,没有一项服务依赖于另一项服务。这会改变GetWorkTimeForDay()的签名,如下所示:

public TimeSpan GetWorkTimeForDay(WorkDay workDay, Date date)
{
    var finalWorkTime = TimeSpan.Zero;

    if (workDay != null)
        finalWorkTime = GetWorkTimeForDay(workDay);

    return finalWorkTime;
}

调用方法看起来与此类似:

public async void Caller(Date date)
{
    ...

    if (ContractCalculationCacheResults.Contains(date))
        return ContractCalculationCacheResults.Get(date);

    var workDay = await DataService.GetWorkDay(date);
    var workTime = Contract.GetWorkTimeForDay(workDay, date);

    ContractCalculationCacheResults.Put(date, workTime);

    ...
}

与其他解决方案一样,缺点是所有调用方法都必须更新(及其调用者,......),即使有时这不是必需的。此外,还将添加许多用于缓存和数据库访问的冗余代码。

从MVVM的角度来看,另一个缺点可能是突然将数据提供给合同服务。因此,我不再使用DI,这或多或少违反了规则。 (这里有关于如何正确地将服务注入服务的问题)

最后但同样重要的是,我还必须从GetWorkTimeForDay方法中删除缓存。只有在之前没有缓存结果时,我才会要求数据库获取工作日并在之后进行计算。

3。 ?

...

实际问题

在基于MVVM的项目中将async服务与非async服务结合起来的优秀设计是什么?

我想保持实际的计算同步,因为它们是CPU绑定的。我想保持数据访问异步,因为它们不受CPU限制,但可能需要一些时间。

所以这篇文章不是关于如何等待await / Task / ...的结果的技术解决方案。它更像是一个关于MVVM的架构问题,注入其他服务的服务,同步和异步服务的组合。

1 个答案:

答案 0 :(得分:3)

  

此外,突然CPU绑定计算变得异步,而不是它应该如何工作......

这背后有一个简单的原因:当人们看到异步方法时,他们通常认为,这个方法将异步执行,因此不会阻塞当前线程。

你的方法会这样:

//Start...             (On current thread)
//Access the database  (On IO Thread, freeing up the current thread)
//Return to current thread
//Do the calculations  (On the current thread)

人们可能不期望的是调用线程上的工作负载。

我在这里使用@ kevin-gosse:只需将其更改为异步方法,因为它仍然有一个IO绑定部分。如果您希望其他人调用该方法,请确保文档中也包含CPU密集型部分。

Stephen Cleary有一些很好的例子on his blog

您的第二种解决方案方法也完全有效。如果你认为你的'合同'纯粹是一个计算引擎,那绝对不应该关心它的数据来自何处。你还有一个脱节的,可测试的课程。