How to abstract type convertion function properly?

55 views Asked by At

I have a lot of methods that look like that:

override suspend fun getBalance(): Result<BigDecimal> = withContext(Dispatchers.IO) {
    Log.d(TAG, "Fetching balance from data store")
    val balance = balancePreferencesFlow.firstOrNull()
        ?: return@withContext Result.Error(CacheIsInvalidException)

    return@withContext when (balance) {
        is Result.Success -> {
            if ((balance.data.timestamp + ttl) <= getCurrentTime()) {
                deleteBalance()
                Result.Error(CacheIsInvalidException)
            } else {
                resultOf { balance.data.toDomainType() }
            }
        }
        is Result.Error -> balance
    }
}

There I am collecting a Flow of some type from DataStore, then if it is a Success Result(with data parameter of type T), I should get its timestamp(it is a data class field), and if the condition is true delete invalid data and if it's false return the converted Result.

The convertion functions look somehow like that:

fun BigDecimal.toPersistenceType(): Balance = Balance(
    balanceAmount = this,
    timestamp = getCurrentTime()
)

fun Balance.toDomainType(): BigDecimal = this.balanceAmount

I've tried to make an abstract method in this way, but I don't completely understand how I should pass a lambda to it.

suspend inline fun <reified T : Any, reified V : Any> getPreferencesDataStoreCache(
    preferencesFlow: Flow<Result<V>>,
    ttl: Long,
    deleteCachedData: () -> Unit,
    getTimestamp: () -> Long,
    convertData: () -> T
): Result<T> {
    val preferencesResult = preferencesFlow.firstOrNull()

    return when (preferencesResult) {
        is Result.Success -> {
            if ((getTimestamp() + ttl) <= getCurrentTime()) {
                deleteCachedData()
                Result.Error(CacheIsInvalidException)
            } else {
                resultOf { preferencesResult.data.convertData() }
            }
        }
        is Result.Error -> preferencesResult
        else -> Result.Error(CacheIsInvalidException)
    }
}

And a lambda for convertion should look like an extension method.

The Result class:

sealed class Result<out T : Any> {

    data class Success<out Type : Any>(val data: Type) : Result<Type>()
    data class Error(val exception: Exception) : Result<Nothing>()
}
1

There are 1 answers

0
Ircover On BEST ANSWER

First of all, I see here some cache work, that from my point should be placed in one interface.

interface Cache {
    val timestamp: Long
    fun clear()
}

You can make timestamp property nullable to return null if your cache is still empty - it's up to you.

Then universal method you need I assume to place inside Result class as it seems to be only its own work.

sealed class Result<out T : Any> {
    data class Success<out Type : Any>(val data: Type) : Result<Type>()
    data class Error(val exception: Exception) : Result<Nothing>()

    fun <R : Any> convertIfValid(cache: Cache, ttl: Long, converter: (T) -> R) : Result<R> =
            when (this) {
                is Success -> {
                    if (cache.timestamp + ttl <= getCurrentTime()) {
                        cache.clear()
                        Error(CacheIsInvalidException())
                    } else {
                        Success(converter(data))
                    }
                }
                is Error -> this
            }
}

May be it would be better to place getCurrentTime method in some injected entity too, but it's not important in this post. By the way, as you can see here in when I didn't place else state as it is unnecessary for sealed classes.

From your code I can make an example of cache implementation only for balance:

class BalanceCache : Cache {
    var balanceValue = Balance()
    override val timestamp: Long
        get() = balanceValue.timestamp

    override fun clear() {
        deleteBalance()
    }
}

If you need more examples from me, please give me more details about your code where you want to use it.