创建业务逻辑来访问数据

优质
小牛编辑
127浏览
2023-12-01

在实现访问服务器和与本地数据库交互之后,是时候把事情整合起来了。逻辑步骤如下:

  • 从数据库获取数据
  • 检查是否存在对应星期的数据
  • 如果有,返回UI并且渲染
  • 如果没有,请求服务器获取数据
  • 结果被保存在数据库中并且返回UI渲染

但是我们的commands不应该去处理所有这些逻辑。数据源应该是一个具体的实现,这样就可以被容易地修改,所以增加一些额外的代码,然后把command从数据访问中抽象出来听起来是个不错的方式。在我们的实现中,它会遍历整个list直到结果被找到。

所以我们先来给接口定义一些我们实现provider需要使用到的数据源:

  1. interface ForecastDataSource {
  2. fun requestForecastByZipCode(zipCode: Long, date: Long): ForecastList?
  3. }

provider需要一个接收zip code和一个date,然后它应该根据那一天返回一周的天气预报。

  1. class ForecastProvider(val sources: List<ForecastDataSource> =
  2. ForecastProvider.SOURCES) {
  3. companion object {
  4. val DAY_IN_MILLIS = 1000 * 60 * 60 * 24
  5. val SOURCES = listOf(ForecastDb(), ForecastServer())
  6. }
  7. ...
  8. }

forecast provider接收一个数据源列表,通过构造函数传入(比如用于测试),但是我设置了source的默认值为被定义在companion object中的SOURCESList。我将使用数据库的数据源和服务端数据源。顺序是很重要的,因为它会根据顺序去遍历这个sources,然后一旦获取到有效的返回值就会停止查询。逻辑顺序是先在本地查询(本地数据库中),然后再通过API查询。

所以主函数的代码如下:

  1. fun requestByZipCode(zipCode: Long, days: Int): ForecastList
  2. = sources.firstResult { requestSource(it, days, zipCode) }

它会得到第一个不是null的结果然后返回。当我在第18章中讲到的大量的函数操作符中搜索后,我没有找到完全符合我想要的。所以当我去查看Kotlin的源码时,我直接拷贝了first函数然后修改它们来达到我想要的目的:

  1. inline fun <T, R : Any> Iterable<T>.firstResult(predicate: (T) -> R?) : R {
  2. for (element in this){
  3. val result = predicate(element)
  4. if (result != null) return result
  5. }
  6. throw NoSuchElementException("No element matching predicate was found.")
  7. }

该函数接收一个断言函数,它接收一个T类型的对象然后返回一个R?类型的值。这表示predicate可以返回null类型,但是我们的firstResult不能返回null。这就是为什么返回R的原因。

它怎么工作呢?它将遍历集合中的每一个元素然后执行这个断言函数。当这个断言函数的结果返回不是null时,这个结果就会被返回。

如果我们可以允许sources返回null,那我们就可以使用firstOrNull函数来代替。不同之处就是最后一行的返回null和抛异常。但是我现在不在代码里面去处理这些细节了。

在我们的例子中T = ForecastDataSourceR = ForecastList。但是记住在ForecastDataSource中指定的函数返回一个ForecastList?,也就是R?,所以一切都是匹配得这么完美。requestSource让前面的函数看起来更有可读性:

  1. fun requestSource(source: ForecastDataSource, days: Int, zipCode: Long):
  2. ForecastList? {
  3. val res = source.requestForecastByZipCode(zipCode, todayTimeSpan())
  4. return if (res != null && res.size() >= days) res else null
  5. }

如果结果不是null并且数量也参数匹配,那这个查询被执行且只会返回一个数据。否则,数据源没有足够的数据来返回一个成功的结果。

函数todayTimeSpan()计算今天毫秒级的时间,并排除掉“时差”。其中一些数据源(我们例子中的数据库)可能会需要它。因为如果我们没有指定更多的信息,服务端默认就是今天,所以我们不需要设置它。

  1. private fun todayTimeSpan() = System.currentTimeMillis() / DAY_IN_MILLIS * DAY_IN_MILLIS

这个类完整的代码如下:

  1. class ForecastProvider(val sources: List<ForecastDataSource> =
  2. ForecastProvider.SOURCES) {
  3. companion object {
  4. val DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
  5. val SOURCES = listOf(ForecastDb(), ForecastServer())
  6. }
  7. fun requestByZipCode(zipCode: Long, days: Int): ForecastList
  8. = sources.firstResult { requestSource(it, days, zipCode) }
  9. private fun requestSource(source: RepositorySource, days: Int,
  10. zipCode: Long): ForecastList? {
  11. val res = source.requestForecastByZipCode(zipCode, todayTimeSpan())
  12. return if (res != null && res.size() >= days) res else null
  13. }
  14. private fun todayTimeSpan() = System.currentTimeMillis() /
  15. DAY_IN_MILLIS * DAY_IN_MILLIS
  16. }

我们已经定义了一个ForecastDb。现在我们需要去实现ForcastDataSource

  1. class ForecastDb(val forecastDbHelper: ForecastDbHelper =
  2. ForecastDbHelper.instance, val dataMapper: DbDataMapper = DbDataMapper())
  3. : ForecastDataSource {
  4. override fun requestForecastByZipCode(zipCode: Long, date: Long) =
  5. forecastDbHelper.use {
  6. ...
  7. }
  8. ...
  9. }

ForecastServer还没有还被实现,但是这是非常简单的。它在从服务端接收到数据之后就会使用ForecastDb去保存到数据库。用这种方式,我们就可以缓存这些数据到数据库中,提供给以后的查询。

  1. class ForecastServer(val dataMapper: ServerDataMapper = ServerDataMapper(),
  2. val forecastDb: ForecastDb = ForecastDb()) : ForecastDataSource {
  3. override fun requestForecastByZipCode(zipCode: Long, date: Long):
  4. ForecastList? {
  5. val result = ForecastByZipCodeRequest(zipCode).execute()
  6. val converted = dataMapper.convertToDomain(zipCode, result)
  7. forecastDb.saveForecast(converted)
  8. return forecastDb.requestForecastByZipCode(zipCode, date)
  9. }
  10. }

它也是使用了之前我们创建的data mapper,最然我们修改一些函数的名字来让它更加与我们之前用在database model的mapper更相似。你可以查看provider来查看细节。

被重写的方法用来请求服务器,转换结果到domain objects并保存它们到数据库。它最后查询数据库返回数据,这是因为我们需要使用到插入到数据库中的字增长id。

这就是provider被实现的最后的一步了。现在我们需要开始使用它。ForecastCommand不会再直接与服务端交互,也不会转换数据到domain model

  1. RequestForecastCommand(val zipCode: Long,
  2. val forecastProvider: ForecastProvider = ForecastProvider()) :
  3. Command<ForecastList> {
  4. companion object {
  5. val DAYS = 7
  6. }
  7. override fun execute(): ForecastList {
  8. return forecastProvider.requestByZipCode(zipCode, DAYS)
  9. }
  10. }

其它修改的地方包括重命名和包的结构调整。在Kotlin for Android Developers repository查看相应的提交。