iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
1
Modern Web

30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發系列 第 20

[Day 20] 遠征 Kotlin × Spring Boot 使用分層架構 Layered Architecture

  • 分享至 

  • twitterImage
  •  

軟體系統架構是建構者賦予系統的樣貌,而該樣貌是由不同元件組合而成,元件之間會有不同的合作與溝通方式,目的是為了讓軟體系統在開發、部署、運行和維護都能輕鬆理解與開發,也讓系統的生命週期成本趨近最小化,使程式設計師生產力最大化。—《Clean Architecture》

而本章將要介紹架構—分層架構(Layered Architecture),又稱為N層架構模式(N-tier Architecture Pattern),是軟體開發中經常看到的架構之一,它的每一層都有自己所負責的任務,每一層也有許多好處,例如:

  • 簡化複雜性,達到關注點分離、結構清晰
  • 降低耦合度,隔離層與層之間的關聯,降低彼此依賴,上層不需要了解下層狀況,利於分工、測試與維護
  • 提高靈活性,可以靈活替換某一層的實作方法
  • 提高擴展性,方便實現分散式部署方法

https://ithelp.ithome.com.tw/upload/images/20200929/20121179aThMFJFPPl.png

而在 Spring Boot 常見的階層架構會將專案分為四個主要類別:

  • 表示層 Presentation Layer

    屬於該架構頂層,主要負責 Http 請求、路由處理、身份驗證與Json資料轉換處理,會將資料傳遞到業務邏輯層進行溝通

  • 業務邏輯層 Business Layer

    主要處理專案所有相關業務邏輯,包含處理業務規則、流程、資料完整性等,並接收來自表示層的資料請求,進行邏輯處理後,會轉向與資料持久層提交請求並傳遞資料結果。

  • 資料持久層 Persistence Layer

    作為應用程式與資料庫之間的抽象層,將業務層需要使用的物件映射到資料庫進行相互轉換與溝通

  • 資料庫層 Database Layer

    主要由資料庫組成,所有資料庫相關操作與設定都會於此層處理

在實作上,可參考下圖《 Spring Boot Flow Architecture》,Client 端會與 Controller 層進行 Http 請求溝通,而 Service 層會針對專案業務邏輯進行處理與請求數據,持久層則是利用 DAO 物件進行資料庫溝通實現,達到不同層處理各自的職責。

https://ithelp.ithome.com.tw/upload/images/20200929/20121179SKLAWjkJUj.png

接下來我們進入實作步驟部份:

  1. 首先在專案內建立 Controller 資料夾並將之前的 Controller 改用 Interface 進行定義,此作法主要是為了解耦合,當我們要修改Controller 實現方法時,只要修改實作 Implement 即可

    • Interface 部份定義需求
    interface StudentController {
    	/**
         * 取得 Student 所有資料
         */
        @GetMapping("/students")
        fun getStudentData(): MutableList<Student>
    
    	/**
         * 新增 Student 資料
         */
        @PostMapping("/students")
        fun addStudentData(@RequestBody student: Student) : Student
    
        /**
         * 利用姓名查詢學生資料
         */
        @PostMapping("/students/search")
        fun getStudentByName(@RequestParam name: String) : ResponseEntity<List<Student>>
    
        /**
         * 修改學生全部資料
         */
        @PutMapping("/students/{id}")
        fun updateStudent(@PathVariable id: Int, @RequestBody student: Student) : ResponseEntity<Student?>
    
        /**
         * 修改學生信箱(欲更新部份資料)
         */
        @PatchMapping("/students/{id}")
        fun updateStudentEmail(@PathVariable id: Int, @RequestBody student: Student): ResponseEntity<Student?>
    
        /**
         * 刪除學生資料
         */
        @DeleteMapping("/students/{id}")
        fun deleteStudent(@PathVariable id: Int): ResponseEntity<Any>
    }
    
    • implement controller 進行實作
    @RestController
    @RequestMapping("/api")
    class StudentControllerImpl(@Autowired val studentDao: StudentDao) : StudentController {
    
        override fun getStudentData(): MutableList<Student> = studentDao.findAll()
    
        override fun addStudentData(student: Student): Student = studentDao.save(student)
    
        override fun getStudentByName(name: String): ResponseEntity<List<Student>>
                = studentDao
                .findByName(name)
                .let {
                    return ResponseEntity(it, HttpStatus.OK)
                }
    
        override fun updateStudent(id: Int, student: Student): ResponseEntity<Student?>
                = studentDao
                .findById(id)
                .run {
                    this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND)
                }.run {
                    return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK)
                }
    
        override fun updateStudentEmail(id: Int, student: Student): ResponseEntity<Student?>
                = studentDao
                .findById(id)
                .run {
                    this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND)
                }
                .run {
                    Student(
                            id = this.id,
                            name = this.name,
                            email = student.email
                    )
                }
                .run {
                    return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK)
                }
    
        override fun deleteStudent(id: Int): ResponseEntity<Any>
                = studentDao
                .findById(id)
                .run {
                    this ?: return ResponseEntity<Any>(null, HttpStatus.NOT_FOUND)
                }
                .run {
                    return ResponseEntity<Any>(studentDao.delete(this), HttpStatus.NO_CONTENT)
                }
    
    }
    
  2. 建立 Data 資料夾存放 DAOEntity 物件,再建立 Service 資料夾準備建立 Service 物件,資料夾結構應如下圖:
    https://ithelp.ithome.com.tw/upload/images/20200930/20121179KkrwO7NMy1.png

  3. 建立 Service 物件 StudentService.kt,建立時如同第一步驟的Controller,先使用 Interface 定義業務邏輯需求再進行實作,最後再將原本的Controller改使用Service進行資料請求,程式如下:

    • Interface 定義業務邏輯需求
    interface StudentService {
    
        /**
         * 查詢所有學生資料
         */
        fun findAllStudent(): MutableList<Student>
    
        /**
         * 新增學生資料
         */
        fun addStudent(student: Student): Student
    
        /**
         * 查詢符合姓名條件的學生資料
         */
        fun findByStudentId(id: Int): Student?
    
        /**
         * 查詢符合姓名條件的學生資料
         */
        fun findByStudentName(name: String): List<Student>
    
        /**
         * 更新學生整個資料
         */
        fun updateStudent(student: Student): Student
    
        /**
         * 更新學生信箱資料
         */
        fun updateStudentEmail(student: Student): Student
    
        /**
         * 刪除學生資料
         */
        fun deleteStudent(student: Student): Unit
    }
    
    • Implement Service 進行實作
    @Service
    class StudentServiceImpl(@Autowired val studentDao: StudentDao) : StudentService {
        override fun findAllStudent(): MutableList<Student> = studentDao.findAll()
    
        override fun addStudent(student: Student): Student =
                Student(
                        name = student.name.trim(),
                        email = student.email.trim()
                ).run {
                    return studentDao.save(this)
                }
    
        override fun findByStudentId(id: Int): Student? = studentDao.findById(id)
    
        override fun findByStudentName(name: String): List<Student> = studentDao.findByName(name)
    
        override fun updateStudent(student: Student): Student =
                Student(
                        id = student.id,
                        name = student.name.trim(),
                        email = student.email.trim()
                ).run {
                    return studentDao.save(this)
                }
    
        override fun updateStudentEmail(student: Student): Student =
                Student(
                        id = student.id,
                        name = student.name,
                        email = student.email.trim()
                ).run {
                    return studentDao.save(this)
                }
    
        override fun deleteStudent(student: Student): Unit = studentDao.delete(student)
    
    }
    
    • 修改 Controller 對業務邏輯層的呼叫請求方法(原先是直接使用 DAO 物件)
    @RestController
    @RequestMapping("/api")
    class StudentControllerImpl(@Autowired val studentService: StudentService) : StudentController {
        /**
         * 取得 Student 所有資料
         */
        override fun getStudentData(): MutableList<Student> = studentService.findAllStudent()
    
        /**
         * 新增 Student 資料
         */
        override fun addStudentData(student: Student): Student = studentService.addStudent(student)
    
        /**
         * 利用姓名查詢學生資料
         */
        override fun getStudentByName(name: String): ResponseEntity<List<Student>>
                = studentService
                .findByStudentName(name)
                .let {
                    return ResponseEntity(it, HttpStatus.OK)
                }
    
        /**
         * 修改學生全部資料
         */
        override fun updateStudent(id: Int, student: Student): ResponseEntity<Student?>
                = studentService
                .findByStudentId(id)
                .run {
                    this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND)
                }.run {
                    return ResponseEntity<Student?>(studentService.updateStudent(this), HttpStatus.OK)
                }
    
        /**
         * 修改學生信箱(欲更新部份資料)
         */
        override fun updateStudentEmail(id: Int, student: Student): ResponseEntity<Student?>
                = studentService
                .findByStudentId(id)
                .run {
                    this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND)
                }
                .run {
                    Student(
                            id = this.id,
                            name = this.name,
                            email = student.email
                    )
                }
                .run {
                    return ResponseEntity<Student?>(studentService.updateStudentEmail(this), HttpStatus.OK)
                }
    
        /**
         * 刪除學生資料
         */
        override fun deleteStudent(id: Int): ResponseEntity<Any>
                = studentService
                .findByStudentId(id)
                .run {
                    this ?: return ResponseEntity<Any>(null, HttpStatus.NOT_FOUND)
                }
                .run {
                    return ResponseEntity<Any>(studentService.deleteStudent(this), HttpStatus.NO_CONTENT)
                }
    
    }
    

此文章有提供範例程式碼在 Github 供大家參考

Reference


上一篇
[Day 19] 遠征 Kotlin × Spring Boot 使用 RESTful API (2)
下一篇
[Day 21] 遠征 Kotlin × Spring Boot 爬蟲實戰教學
系列文
30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言