在基于MVVMLight的应用程序中,我使用一些视图和服务来进行一些工作时间计算。其中有以下服务......
现在,让我们看一下以下方法(位于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限制。所以这没有意义。
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帖子(和其他人)。
因此,我想到的下一个解决方案是分离服务。换句话说,没有一项服务依赖于另一项服务。这会改变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
方法中删除缓存。只有在之前没有缓存结果时,我才会要求数据库获取工作日并在之后进行计算。
...
在基于MVVM的项目中将async
服务与非async
服务结合起来的优秀设计是什么?
我想保持实际的计算同步,因为它们是CPU绑定的。我想保持数据访问异步,因为它们不受CPU限制,但可能需要一些时间。
所以这篇文章不是关于如何等待await
/ Task / ...的结果的技术解决方案。它更像是一个关于MVVM的架构问题,注入其他服务的服务,同步和异步服务的组合。
答案 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。
您的第二种解决方案方法也完全有效。如果你认为你的'合同'纯粹是一个计算引擎,那绝对不应该关心它的数据来自何处。你还有一个脱节的,可测试的课程。