iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Mobile Development

Jetpack Compose X Android Architecture X Functional Reactive Programming系列 第 23

Re-architect with UseCase driven design

Re-architect

大家應該都很常說,或是很習慣使用到一個詞 - 重構(Refactoring)。但是大家在說“重構”的時候其實不太像是在做“重構”,比較像是“重組”或是“重新架構(Re-architect)” 。真正的重構應該是要建立在有良好的測試覆蓋率底下所運行的,而且在重構之後,程式的行為不應該有一絲一毫的改變,在確定重構完成之後,跑過完所有單元測試通過,再來新增新功能來改變程式的行為。

經過以上的說明,相信讀者應該都已經理解接下來我要做的事不是重構(Refactoring),而是重新架構(Re-architect)。原因是因為這個專案到目前為止也沒有寫過任何的單元測試,所以我沒辦法保證改完程式碼之後,所有的行為會維持的一模一樣。雖然說是這樣說,但我們還是要盡最大的努力來保持程式碼的運作機制來是一樣的。

UseCase Driven Design

其實這個名詞只是為了跟之後要介紹的 Domain Driven Design 要做出比較不一樣的區別,上 Google 查了一下才發現原來業界已經有了這個詞了,為了避免誤人子弟,說我亂用名詞,我這邊做了一個比較不一樣的區別,就是我的 UseCase 是兩個字合在一起的,業界的名詞是 "Use case" ,兩個單字是分開的(根本是在硬凹XD)。

好了,名詞都不是重點,這邊要示範的是,用前一篇所介紹的 Clean architecture ,大家所認識的 UseCase 為核心所設計出來的架構會是長怎樣,所以我才會稱為這是 UseCase Driven Design。

第三階段最大的問題

目前所遇到第三階段的需求是,想要能夠放大、縮小整個白板,一個螢幕上顯示的便利貼數量增加了,因此繼續使用之前的做法會有很大的效能負擔:

val allNotes: Observable<List<Note>> = noteRepository.getAllNotes()
// 就算是只有一個便利貼往左位移了一個單位,還是會產生一整個 List 的資料給 View 去顯示。

那我們能怎麼解決這個效能問題呢?首先我們來看看我們是怎麼使用這 App 的:

  1. 將手指放到便利貼上面,持續不斷的觸碰不離開螢幕,該便利貼便會隨著手指的位置而跟著移動。
  2. 點擊某張便利貼,開啟選擇狀態,點選選單中的刪除按鈕,之後就會看到該張便利貼被刪掉了。
  3. 點擊某張便利貼,開啟選擇狀態,點選選單中的某個顏色,之後就會看到該張便利貼的背景顏色換了。

以上這些描述,其實就是便利貼這個 App 的 Use case ,在這邊我想問一下各位讀者:請問讀者在看這些 Use case 的時候,有看到描述兩個以上便利貼的 Use case 嗎?好像沒有,對吧?那我們為什麼要一直使用 List 這個資料結構來顯示所有的便利貼呢?答案也很簡單,因為需要顯示在可見範圍中的所有便利貼,於是我們有另外一個 Use case:

  1. 在可見範圍中,需要能看到所有在範圍內便利貼。

現在再換個角度想想,如果我們一直都在討論“便利貼”,而不是“便利貼們”的話,為什麼不讓他們各自管理呢?便利貼也有自己的 ViewModel ,這樣一來,不管在哪一個便利貼中的哪個欄位被更新了,其他便利貼都不會受到影響不是嗎?像下圖這種感覺:

ABC.png

在這種情況下,我們是可以完成第四個 Use Case 的,因為我們所關注的就只是“有沒有”這些便利貼,而不是這些便利貼“是什麼顏色”,或是“到底位置在哪裡”,換句話說,只要有一個 Id 就可以了:

// Use case 4: 
fun loadAllNotes(): Observable<List<String>>
==>
class LoadAllNotesUseCase {
    fun execute(): Observable<List<String>> { ... }
}

只要拿到這些 ID ,我們就可以產生相對應的 StickyNoteView,而在 StickyNoteView 被產生的同時,也會建立相對應的 NoteViewModel


@Composable
fun StickyNote(noteId: String) {
    val noteViewModel by viewModel<NoteViewModel>() { parametersOf(noteId) }
    val note by noteViewModel.note.subscribeAsState() 
    ...
    ...
}

class NoteViewModel(
    private val noteId: String, 
    private val getNoteUseCase: GetNoteUseCase
) {

    val note: Observable<Note> = getNoteUseCase.execute(noteId)
}

接著,對於每一個便利貼來說,顏色跟位置的更新是重要的,於是第五個 Use case 就出現了:

  1. 在螢幕中顯示的便利貼,要反應現在的最新狀態並正確顯示。
// Use case 5: 
fun getNoteById(noteId: String): Observable<Note>
==>
class GetNoteUseCase {
    fun execute(noteId: String): Observable<Note> { ... }
}

好了,目前看起來最大的問題已經解決了,那上面其他三個 Use case 呢?

// Use case 1: 
fun moveNote(noteId: String, delta: Position)
==>
class MoveNoteUseCase {
    fun execute(noteId: String, delta: Position) { ... }
}

// Use case 2:
fun deleteNote(noteId: String)
==>
class DeleteNoteUseCase {
    fun execute(noteId: String) { ... }
}

// Use case 3:
fun selectColor(noteId: String, color: YBColor)
==>
class SelectColorUseCase {
    fun execute(noteId: String, color: YBColor) { ... }
}

刪除跟選擇顏色有一個比較大的問題,那就是便利貼的選擇狀態會是 Use case 中的一部分嗎?想一想之後可能會覺得不太是,因為這不應該是商業邏輯核心,只能算是 UI 的暫時狀態,所以就只能繼續放在 EditorViewModel 了。

如果是這樣的話,在 EditorViewModel 中,這個選擇狀態會需要跟 Use case 2 還有 Use case 3 做互動,因此 EditorViewModel 會有一部分的程式碼是在處理這樣的邏輯,就不會因為有多了 UseCase 層而少做了什麼事。

其他還有像是水平移動、放大縮小應該會是屬於 EditorViewModel 這邊會去觸發的 Use case ,所以綜合分析下來,架構圖會是長的像下面這樣:

48A13AF0-9D38-4103-9A04-2EFB9716F819.jpg

  • 紅色的箭頭是建立單個便利貼的流程
  • 藍色的箭頭指的是相依性
  • 黑色的字指的是 UseCase

完成了設計之後我們先不要急著實作,可以先嘗試其他更多不同的可能性,接著比較不同可能性的優缺點,最後再下手實作,明天將會跟大家介紹 Domain Driven Design 如何在這個 App 派上用場!


上一篇
Clean architecture in Android
下一篇
初探 Domain driven design
系列文
Jetpack Compose X Android Architecture X Functional Reactive Programming30

尚未有邦友留言

立即登入留言