iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Kotlin

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

[小鎮] 實作三層架構 - 拆拆拆

  • 分享至 

  • xImage
  •  

前情提要

今天我們把三層式架構給做了出來,Service層放業務邏輯,Repo層放與外部接觸如資料庫、外部系統的地方,Controller層專心做User打過來的請求服務。

Repo

package Repo

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import model.*

object CustomerRepo {
    private val customerList = mutableListOf<Customer>()

    fun getCustomer(): Either<MyError.CustomerNotFoundError, List<Customer>> =
        if (customerList.isEmpty()) {
            MyError.CustomerNotFoundError(customerId = "all").left()
        } else {
            customerList.right()
        }

    fun addCustomer(customer: Customer) =
        customerList.add(customer).let { customer }

    fun findCustomerIndex(customer: Customer): Either<MyError.CustomerNotFoundError, Int> =
        when (val index = customerList.indexOfFirst { it.name == customer.name }) {
            -1 -> MyError.CustomerNotFoundError(customerId = index.toString()).left()
            else -> {
                index.right()
            }
        }

    fun updateCustomer(index: Int, customer: Customer) =
        customerList.set(index, customer)

    fun deleteCustomer(index: Int) =
        customerList.removeAt(index)
}



Service

package service

import Repo.CustomerRepo
import Repo.CustomerRepo.addCustomer
import Repo.CustomerRepo.deleteCustomer
import Repo.CustomerRepo.findCustomerIndex
import Repo.CustomerRepo.updateCustomer
import arrow.core.*
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.zipOrAccumulate
import model.*

object CustomerService {
    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(),
            )
        }

    fun getCustomer(): Either<MyError.CustomerNotFoundError, List<Customer>> =
        CustomerRepo.getCustomer()

    fun createCustomer(customerVO: CustomerVO): Either<MyError.ValidateCustomerError, Customer> =
        validateCustomer(customerVO).map {
            addCustomer(it)
        }

    fun updateCustomer(customerVO: CustomerVO) =
        validateCustomer(customerVO).flatMap { validatedCustomer ->
            findCustomerIndex(validatedCustomer).map {
                Pair(it, validatedCustomer)
            }
        }.map { (index: Int, customer: Customer) ->
            updateCustomer(index, customer)
        }

    fun deleteCustomer(customerVO: CustomerVO) =
        validateCustomer(customerVO).flatMap { validatedCustomer ->
            findCustomerIndex(validatedCustomer).map {
                Pair(it, validatedCustomer)
            }
        }.map { (index: Int, customer: Customer) ->
            deleteCustomer(index)
        }
}


controller

package controller

import arrow.core.flatMap
import kotlinx.serialization.json.Json
import model.CustomerVO
import model.MyError
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import service.CustomerService
import service.CustomerService.getCustomer
import utils.decodeFromStringEither
import utils.encodeToStringEither

@RestController
@SpringBootApplication
open class CustomerController {

    @GetMapping("/api/v1/customers")
    fun getAllCustomers(): ResponseEntity<String> {
        return getCustomer().flatMap { customerList ->
            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 {
            CustomerService.createCustomer(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 {
            CustomerService.updateCustomer(it)
        }.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 {
            CustomerService.deleteCustomer(it)
        }.fold(
            ifLeft = {
                MyError.errorToResonse(it)
            },
            ifRight = { ResponseEntity.ok("Delete success") },
        )
    }
}


分層的目的

這邊我們再複習一下分層的目的,

可維護性

分層有助於提高程式碼的可維護性,每個層級都有各自的職責,使開發者更容易定位和修改特定功能。這也使得單元測試更容易執行(之後我們就會來寫囉XD)

可擴展性

分層的話,我們可以獨立擴展或修改每個層級,而不會影響其他層級。代表我們可以根據需求曾加新的功能或服務,而無需對整個應用程式進行大規模修改。減少Side effect的發生

團隊合作

分層也有助於團隊合作,我們可以分別負責不同的層級來開發,這樣可以平行的進行,加快開發進度。

總結

今天我們將三層式架構拆出來囉,之後如果有什麼變動就是比較容易做改變,進一步節省我們的時間XD

像是今天如果我們要換一個資料庫的話,我們只要將Repo層抓出去重寫,接著就可以抽換囉,這樣我們的Service層完全不會受到干擾,而不會發生改變都寫在同一個function,可能會影響到原本的邏輯。


上一篇
[小鎮] 分層吧,蛋糕(? - 三層架構
下一篇
[小城鎮] 實際儲存資料 - Mongodb
系列文
Kotlin魔法:Spring Boot 3的fp奇幻冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言