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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      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 進行實作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      @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 定義業務邏輯需求

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      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 進行實作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      @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 物件)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      @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