From 2b6ed50ec3b014e389194ff23a47761a426b1282 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 9 May 2022 12:18:31 +0300 Subject: [PATCH] WIP: Dev Guidelines improvements --- .../action/viewmodel/home/DueTrnsInfoAct.kt | 31 ++- docs/Developer-Guidelines.md | 178 +++++++++++++++++- 2 files changed, 191 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt index 1cf898d960..1920efdd72 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt @@ -26,22 +26,22 @@ class DueTrnsInfoAct @Inject constructor( private val exchangeAct: ExchangeAct ) : FPAction() { - override suspend fun Input.compose(): suspend () -> Output = suspend { - range - } then dueTrnsAct then { trns -> - val dateNow = dateNowUTC() - trns.filter { - this.dueFilter(it, dateNow) - } - } then { dueTrns -> - //We have due transactions in different currencies - val exchangeArg = ExchangeTrnArgument( - baseCurrency = baseCurrency, - exchange = ::actInput then exchangeAct, - getAccount = accountByIdAct.lambda() - ) + override suspend fun Input.compose(): suspend () -> Output = + suspend { + range + } then dueTrnsAct then { trns -> + val dateNow = dateNowUTC() + trns.filter { + this.dueFilter(it, dateNow) + } + } then { dueTrns -> + //We have due transactions in different currencies + val exchangeArg = ExchangeTrnArgument( + baseCurrency = baseCurrency, + exchange = ::actInput then exchangeAct, + getAccount = accountByIdAct.lambda() + ) - io { Output( dueIncomeExpense = IncomeExpensePair( income = sumTrns( @@ -58,7 +58,6 @@ class DueTrnsInfoAct @Inject constructor( dueTrns = dueTrns ) } - } data class Input( val range: ClosedTimeRange, diff --git a/docs/Developer-Guidelines.md b/docs/Developer-Guidelines.md index 0d1b16a871..64591d7f36 100644 --- a/docs/Developer-Guidelines.md +++ b/docs/Developer-Guidelines.md @@ -134,7 +134,7 @@ Actions accept `Action Input`, handles `threading`, abstract `side-effects` (IO) - `FPAction()`: declaritve FP style _(preferable)_ - `Action()`: imperative OOP style -**Action Lifecycle:** +**Action Graph:** ```mermaid graph TD; @@ -162,14 +162,185 @@ action -- abstracted IO --> pure -- Result --> action action -- Final Result --> output ``` +**Action Composition Examples** + +_Calculate Balance_ +```Kotlin +//Example 1: Calculates Ivy's balance +class CalcWalletBalanceAct @Inject constructor( + private val accountsAct: AccountsAct, + private val calcAccBalanceAct: CalcAccBalanceAct, + private val exchangeAct: ExchangeAct, +) : FPAction() { + + override suspend fun Input.compose(): suspend () -> BigDecimal = recipe().fixUnit() + + private suspend fun Input.recipe(): suspend (Unit) -> BigDecimal = + accountsAct thenFilter { + withExcluded || it.includeInBalance + } thenMap { + calcAccBalanceAct( + CalcAccBalanceAct.Input( + account = it, + range = range + ) + ) + } thenMap { + exchangeAct( + ExchangeAct.Input( + data = ExchangeData( + baseCurrency = baseCurrency, + fromCurrency = (it.account.currency ?: baseCurrency).toOption(), + toCurrency = balanceCurrency + ), + amount = it.balance + ) + ) + } thenSum { + it.orNull() ?: BigDecimal.ZERO + } + + data class Input( + val baseCurrency: String, + val balanceCurrency: String = baseCurrency, + val range: ClosedTimeRange = ClosedTimeRange.allTimeIvy(), + val withExcluded: Boolean = false + ) +} +``` + +_Overdue Transactions_ +```Kotlin +//Example 2: Due transtions + due income/expense for a given filter +class DueTrnsInfoAct @Inject constructor( + private val dueTrnsAct: DueTrnsAct, + private val accountByIdAct: AccountByIdAct, + private val exchangeAct: ExchangeAct +) : FPAction() { + + override suspend fun Input.compose(): suspend () -> Output = + suspend { + range + } then dueTrnsAct then { trns -> + val dateNow = dateNowUTC() + trns.filter { + this.dueFilter(it, dateNow) + } + } then { dueTrns -> + //We have due transactions in different currencies + val exchangeArg = ExchangeTrnArgument( + baseCurrency = baseCurrency, + exchange = ::actInput then exchangeAct, + getAccount = accountByIdAct.lambda() + ) + + Output( + dueIncomeExpense = IncomeExpensePair( + income = sumTrns( + incomes(dueTrns), + ::exchangeInBaseCurrency, + exchangeArg + ), + expense = sumTrns( + expenses(dueTrns), + ::exchangeInBaseCurrency, + exchangeArg + ) + ), + dueTrns = dueTrns + ) + } + + data class Input( + val range: ClosedTimeRange, + val baseCurrency: String, + val dueFilter: (Transaction, LocalDate) -> Boolean + ) + + data class Output( + val dueIncomeExpense: IncomeExpensePair, + val dueTrns: List + ) +} + + +//Example 3: Overdue transactions + their income/expense +class OverdueAct @Inject constructor( + private val dueTrnsInfoAct: DueTrnsInfoAct +) : FPAction() { + + override suspend fun Input.compose(): suspend () -> Output = suspend { + DueTrnsInfoAct.Input( + range = ClosedTimeRange( + from = beginningOfIvyTime(), + to = toRange + ), + baseCurrency = baseCurrency, + dueFilter = ::isOverdue + ) + } then dueTrnsInfoAct then { + Output( + overdue = it.dueIncomeExpense, + overdueTrns = it.dueTrns + ) + } + + data class Input( + val toRange: LocalDateTime, + val baseCurrency: String + ) + + data class Output( + val overdue: IncomeExpensePair, + val overdueTrns: List + ) +} +``` + > Actions are very similar to the "use-cases" from the standard "Clean Code" architecture. -> You can compose actions and pure functions by using **`then`**. +> Tip: You can compose actions and pure functions by using **`"then"`**, **`"thenMap"`**, **`"thenFilter"`**, **`"thenSum"`**. + +> Tip: When creating an `Action` make it as **atomic** as possible. The goal of each `Action` is to do one thing **efficiently** and to be **composable** with other actions like LEGO. ### 4. Pure (domain logic with pure code) The `pure` layer as the name suggests must consist of only pure functions without side-effects. If the business logic requires, **side-effects must be abstracted**. +**Function types** +- Partial: not defined for all input values +```Kotlin +@Partial(inCaseOf="b = 0, produces ArithmeticException::class") +fun divide(a: Int, b: Int) = a / b +``` +- Total: defined for all input values but with side-effects +```Kotlin +@Total +fun +``` + +Each `@Pure` function must be **total** and its `@SideEffect`(s) if any abstracted. + +> Rule: If a pure function is called with the **same input** and mocked side-effects it must always produce the **same output**. + +**Pure graph** +```mermaid +graph TD; + +input(Input) +pure(Pure) +side-effect(IO / Side-Effect) +lambda("@SideEffect Lambda") +output(Output) + +side-effect -- Implements --> lambda + +input -- Data --> pure +lambda -- Abstracted Effects --> pure + +pure -- Calculates --> output +``` + **Code Example** ```Kotlin //domain.action @@ -203,9 +374,12 @@ suspend fun exchange( getExchangeRate: suspend (baseCurrency: String, toCurrency: String) -> ExchangeRate?, ): Option { //PURE IMPLEMENTATION + //.... } ``` +> Tip: Make `pure` functions small, atomic and composable. + ### 5. UI (@Composable) Renders the `UI State` that the user sees, handles `user input` and transforms it to `events` which are propagated to the `ViewModel`. **Do NOT perform any business logic or computations.**