iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Kotlin

Kotlin魔法:Spring Boot 3的fp奇幻冒險系列 第 9

[小草原] 錯誤處理超進化-Either 實戰

  • 分享至 

  • xImage
  •  

前情提要

我們昨天介紹了Either是什麼好東西,今天我們要使用Either來改寫我們的程式碼,首先我們要引入kotlin fp的好套件,Arrow-KT。

Arrow-KT簡介

Arrow-KT是一個功能強大的fp library,專為Kotlin語言設計,它目的是要讓FP(函數式程式設計)在Kotlin中更加容易和有效,使得在Kotlin中進行fp變得更加自然和直觀。

它有幾項特點
不可變性: Arrow-KT鼓勵不可變性,這是函數式程式設計的一個重要原則。
函數組合: 讓我們能夠輕鬆創建、組合和轉換函數。這使我們更容易編寫更具可重用性和組合性的程式碼。

適用於副作用: 有IO type來處理關於外部的side effect

Pom

<dependency>
  <groupId>io.arrow-kt</groupId>
  <artifactId>arrow-core</artifactId>
  <version>1.2.0</version>
</dependency>
<dependency>
  <groupId>io.arrow-kt</groupId>
  <artifactId>arrow-fx-coroutines</artifactId>
  <version>1.2.0</version>
</dependency>

Code

package controller

import arrow.core.*
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.zipOrAccumulate
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import model.*
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import utils.decodeFromStringEither
import utils.encodeToStringEither

fun customerVOToCustomer(customerVO: CustomerVO) =
    when {
        customerVO.contactInfo.contains("@") -> Customer(
            Name(customerVO.name),
            Email(customerVO.contactInfo),
            Age(customerVO.age)
        )

        else -> Customer(Name(customerVO.name), Phone(customerVO.contactInfo), Age(customerVO.age))

    }

fun validateCustomer(customerVo: CustomerVO): Either<MyError.ValidateCustomerError, Customer> =
    either<NonEmptyList<MyError.ValidateCustomerError>, Customer> {
        zipOrAccumulate(
            { ensure(customerVo.contactInfo.length <= 8) { MyError.ValidateCustomerError("contactInfo Too Long") } },
            { ensure(customerVo.name.isNotBlank()) { MyError.ValidateCustomerError("Name is blank") } }
        ) { _, _ ->
            customerVOToCustomer(customerVo)
        }
    }.mapLeft {
        MyError.ValidateCustomerError(
            it.map { validateCustomerError ->
                validateCustomerError.value
            }.joinToString()
        )
    }


@RestController
@SpringBootApplication
open class MyApplication {

    private val customerList = mutableListOf<Customer>()

    @GetMapping("/api/v1/customers")
    fun getAllCustomers(): ResponseEntity<String> {
        if (customerList.isEmpty())
            return MyError.errorToResonse(MyError.CustomerNotFoundError(customerId = "all"))
        return Json.encodeToStringEither(customerList).fold(
            ifLeft = { (e) -> MyError.errorToResonse(MyError.JsonEncodeError(e)) },
            ifRight = { ResponseEntity.ok(it) }
        )
    }

    @PostMapping("/api/v1/customers")
    fun createCustomer(@RequestBody newCustomer: String): ResponseEntity<String> {
        return Json.decodeFromStringEither<CustomerVO>(newCustomer).flatMap {
            validateCustomer(it)
        }.map {
            customerList.add(it)
            it
        }.flatMap {
            Json.encodeToStringEither(it)
        }.fold(
            ifLeft = { MyError.errorToResonse(it) },
            ifRight = { ResponseEntity.ok(it) }
        )
    }


    @PutMapping("/api/v1/customers")
    fun updateCustomer(@RequestBody newCustomer: String): ResponseEntity<String> {
        return Json.decodeFromStringEither<CustomerVO>(newCustomer).flatMap {
            validateCustomer(it)
        }.flatMap { validatedCustomer ->
            when (val index = customerList.indexOfFirst { it.name == validatedCustomer.name }) {
                -1 -> MyError.CustomerNotFoundError(customerId = index.toString()).left()
                else -> {
                    customerList.set(index, validatedCustomer)
                    index.right()
                }
            }
        }.fold(
            ifLeft = { MyError.errorToResonse(it) },
            ifRight = { ResponseEntity.ok("Modify success") }
        )
    }

    @DeleteMapping("/api/v1/customers")
    fun DeleteCustomer(@RequestBody newCustomer: String): ResponseEntity<String> {
        return Json.decodeFromStringEither<CustomerVO>(newCustomer).flatMap {
            validateCustomer(it)
        }.flatMap { validatedCustomer ->
            when (val index = customerList.indexOfFirst { it.name == validatedCustomer.name }) {
                -1 -> {
                    MyError.CustomerNotFoundError(customerId = index.toString()).left()
                }

                else -> {
                    customerList.removeAt(index)
                    index.right()
                }
            }
        }.fold(
            ifLeft = {
                MyError.errorToResonse(it)
            },
            ifRight = { ResponseEntity.ok("Delete success") }
        )
    }
}

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

Error

package model

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.springframework.http.ResponseEntity

sealed class MyError : Error() {
    @Serializable
    sealed class ValidateError : MyError()

    @Serializable
    sealed class ServerError : MyError()


    @Serializable
    data class ValidateCustomerError(val value: String) : ValidateError()

    @Serializable
    data class DBConnectError(@Contextual val e: Error) : ServerError()

    @Serializable
    data class JsonEncodeError(@Contextual val e: Throwable) : ServerError()

    @Serializable
    data class JsonDecodeError(@Contextual val e: Throwable) : ServerError()


    @Serializable
    data class CustomerNotFoundError(val customerId: String) : MyError()

    companion object {
        fun errorToResonse(myError: MyError): ResponseEntity<String> {
            return when (myError) {
                is DBConnectError ->
                    ResponseEntity.status(500).body("DB Connect Error: ${myError.e}")

                is JsonEncodeError ->
                    ResponseEntity.status(500).body("Json Encoder Error: ${myError.e}")

                is JsonDecodeError ->
                    ResponseEntity.status(500).body("Json Decoder Error: ${myError.e}")

                is ValidateCustomerError ->
                    ResponseEntity.status(400).body("validation Error: ${myError.value}")

                is CustomerNotFoundError ->
                    ResponseEntity.status(404).body("ID: ${myError.customerId} Not Found!")

            }

        }
    }
}

總結

我們今天將Either給寫了出來,只要Either的過程裡面有一個錯誤,他就會噴左邊,這樣我們就可以將錯誤接收下來,最後就會回Error的Response,另外我們使用了Validation,並且將錯誤積累起來,這樣就可以一次印出錯誤的內容。

透過Either,我們實現了鐵道式的程式設計,讓我們的邏輯變得很清晰,基本上我們不需要關注錯誤的那條路,只要將錯誤設定好,就可以放心管正確那條路就好。

參考資料

https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/


上一篇
[小草原] 錯誤處理超進化-Either
下一篇
[小鎮] 分層吧,蛋糕(? - 三層架構
系列文
Kotlin魔法:Spring Boot 3的fp奇幻冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言