Search code examples
kotlintestingfunctional-programming

What should happen after side effect is removed in a function?


As part of The Joy of Kotlin book (page 9). it comes up with the below idea to remove side effects from a function, but I couldn't figure out where we should do the charge() functionality after that. Now let me bring the code:

Suppose we have a buyDonut function:

fun buyDonut(creditCard: CreditCard): Donut {
   val donut = Donut()
   creditCard.charge(donut.price)   // Charges the credit card as a side effect
   return donut
}

To remove the side effect we update it as follows:

fun buyDonut(creditCard: CreditCard): Purchase {
   val donut = Donut()
   val payment = Payment(creditCard, Donut.price)
   return Purchase(donut, payment)
}

Here is the Payment and Purchase classes definition:

class Payment(val creditCard: CreditCard, val amount: Int)
data class Purchase(val donut: Donut, val payment: Payment)

The benefit of doing this is we can test the buyDonut function without the need to call CreditCard.charge() or use mock. It could be done like this:

@Test
fun testBuyDonuts() {
   val creditCard = CreditCard()
   val purchase = buyDonuts(creditCard)
   assertEquals(Donut.price, purchase.payment.amount)
   assertEquals(creditCard, purchase.payment.creditCard)
}

So far so good, but where should charge() method get called? It seems just moving things around but at last, I have to use mocking somewhere else where the charge() method is going to be called, or there is an approach that I can't figure out.


Solution

  • You're right; removing the side effect from buyDonut() doesn't eliminate the need for a charge() operation. Instead, it defers that operation to a later point where you can handle it more systematically.

    Typically, you'd collect multiple Payment objects and process them in a batch, maybe in a function like processPayments():

    fun processPayments(payments: List<Payment>) {
        payments.forEach { payment ->
            payment.creditCard.charge(payment.amount)
        }
    }
    

    This function can be tested independently and will be the place where you'd use mocks if necessary. This separation allows for easier testing and more flexible code, e.g., adding additional steps like logging or validation before actually charging.