我們昨天介紹了Either是什麼好東西,今天我們要使用Either來改寫我們的程式碼,首先我們要引入kotlin fp的好套件,Arrow-KT。
Arrow-KT是一個功能強大的fp library,專為Kotlin語言設計,它目的是要讓FP(函數式程式設計)在Kotlin中更加容易和有效,使得在Kotlin中進行fp變得更加自然和直觀。
它有幾項特點
不可變性: Arrow-KT鼓勵不可變性,這是函數式程式設計的一個重要原則。
函數組合: 讓我們能夠輕鬆創建、組合和轉換函數。這使我們更容易編寫更具可重用性和組合性的程式碼。
適用於副作用: 有IO type來處理關於外部的side effect
<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>
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)
}
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/