Data layer implementation (2)

上一篇的 repository 还欠一个 mapper 把 EtaResponse 转成 EtaResult。我们首先准备一个通用的 interface:

interface Mapper<T, R> {
    suspend fun map(o: T): R
}

有些 Android Clean Architecture 会每个 layer 都准备一个对应的 mapper interface,这次示范我们就简化这一部分,全部 layer 都共用同一个 mapper interface,不论是由高层次 layer 去低层次 layer 还是由低层次 layer 去高层次 layer 都一样。因为这个 mapper 都是为了在写 unit test 时可以 mock 那个 interface 而不是 mock 那个 concrete implementation,所以它是不是共用 interface 问题不大。

以下是整个 mapper class 的 code:

private val TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

class EtaResponseMapper @Inject constructor() : Mapper<HttpResponse, EtaResult> {
    override suspend fun map(o: HttpResponse): EtaResult = when (o.status) {
        HttpStatusCode.OK -> mapResponse(o.receive())
        HttpStatusCode.TooManyRequests -> EtaResult.TooManyRequests
        HttpStatusCode.InternalServerError -> EtaResult.InternalServerError
        else -> EtaResult.Error(IllegalStateException("Unsupported HTTP status code ${o.status}"))
    }

    private fun mapResponse(response: EtaResponse): EtaResult = with(response) {
        when {
            status == EtaResponse.STATUS_ERROR_OR_ALERT -> EtaResult.Incident(
                message = message,
                url = url,
            )
            isDelay == EtaResponse.IS_DELAY_TRUE -> EtaResult.Delay
            else -> EtaResult.Success(
                schedule = sequence {
                    yieldAll(data.values.asSequence()
                        .flatMap { it.up }
                        .map { mapEta(EtaResult.Success.Eta.Direction.UP, it) })
                    yieldAll(data.values.asSequence()
                        .flatMap { it.down }
                        .map { mapEta(EtaResult.Success.Eta.Direction.DOWN, it) })
                }.toList()
            )
        }
    }

    private fun mapEta(direction: EtaResult.Success.Eta.Direction, eta: EtaResponse.Eta) =
        with(eta) {
            EtaResult.Success.Eta(
                direction = direction,
                platform = plat,
                time = try {
                    ZonedDateTime.of(
                        LocalDateTime.parse(time, TIMESTAMP_FORMATTER),
                        DEFAULT_TIMEZONE
                    ).toInstant()
                } catch (e: DateTimeParseException) {
                    Instant.EPOCH
                },
                destination = try {
                    Station.valueOf(dest)
                } catch (e: IllegalArgumentException) {
                    Station.UNKNOWN
                },
                sequence = seq.toIntOrNull() ?: 0,
            )
        }
}

虽然看起来很长,但做的东西其实很简单:首先是看 HTTP response status code,如果是 429500 可以马上回传 EtaResult.TooManyRequestsEtaResult.InternalServerError,不用再花时间看 response body。至於 200 就要看 response body 才能知道要回传甚麽(即是 mapResponse 的部分)。

去到 mapResponse,我们先把易处理的东西处理,例如 statusisDelay 两个 property。EtaResult.IncidentEtaResult.Delay 就是靠这两个 property 来判断。最後剩下的就是最平常的情况,那就是 response 有提供列车班次。

mapEta 入面是把 EtaResponse.Eta 转换成 EtaResult.Success.Eta。Response JSON object 是用 UPDOWN array 区别上下行班次,但我们把两个 array 合并成一个 list,方便日後 UI 可以自订排序。时间方面,我们把原来是疑似 ISO 8601 格式的 string 的日期时间转成 Instant。由於它不是正式的 ISO 8601 格式,我们要针对这个格式写一个 DateTimeFormatter (TIMESTAMP_FORMATTER) 转换换成 LocalDateTime 然後再转成 ZonedDateTime 最後转换成 InstantDEFAULT_TIMEZONE 是定义在另一个档案:

val DEFAULT_TIMEZONE: ZoneId = ZoneId.of("Asia/Hong_Kong")

不要忘记还有一样东西要做的是要在 DataModule 加回 @Binds 的 function,否则在用的时候 Dagger 会报错:

@Binds
fun bindEtaResponseMapper(mapper: EtaResponseMapper): Mapper<HttpResponse, EtaResult>

到了这里,data layer 的实作就完成了。完整的 code 可以直接去 GitHub repo 查阅。


<<:  Day 0x16 UVa10235 Simply Emirp

>>:  Day 23 「启动!Outside-In 之路」Controller 与单元测试

npm
杂谈    

[FGL] 4GL程序的资源档Resource file体系

也学过 OPEN WINDOW了,可是为什麽 T 产品可以有TOOLBAR和 TOPMENU 我这...

Day05-Variables

前言 在我们之前的练习都只有使用var宣告变数,其实还有其它两个宣告方式可以使用。 接下来我们会学习...

Spring Framework X Kotlin Day 29 Observability

GitHub Repo https://github.com/b2etw/Spring-Kotlin...

从pyside2 快速移植到pyside6的方法

目前稳定的主流是PyQt 5, PySide2 也是对应到这个版本。但从下个版本开始就改成 PySi...

结语: AI平台的六个ING

终於~~ 今天是第三十天,很开心跟大家分享AI平台的Labeling、Training、Tracki...