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 派上用场!


<<:  处理 API 层次感之地基篇

>>:  [ 卡卡 DAY 13 ] - React Native 页面导览 Navigation (上)

尺寸单位 px、em、rem

前言 在现实生活中,常见的尺寸单位有公分(cm)、公尺(m)、奈米(nm), 而在网页画面中自己的尺...

[Day4] Rust 闭包以及判断式

话就不多说了,直接开始今天的内容吧 闭包 闭包的别称为「匿名函数」有三个特点 可以像函数一样被呼叫 ...

[DAY 28] 章节3-8: 前往农场前夕- k-means(k平均分类演算法) (2/2)

3-8 前往农场前夕 「设定的方法有很多种,如果是已经知道群数的话,就可以设定k为该群数,让k-me...

OAuth 2.0

Golang OAuth 2.0 在一开始的开赛目标就是希望可以完成golang + OAuth 2...

Day27 - 云端交易主机 - Ubuntu SSH登入 & 远端桌面

云端交易主机 - Ubuntu SSH登入 & 远端桌面 SSH登入 本机端建立SSH金钥 ...