大家应该都很常说,或是很习惯使用到一个词 - 重构(Refactoring)。但是大家在说“重构”的时候其实不太像是在做“重构”,比较像是“重组”或是“重新架构(Re-architect)” 。真正的重构应该是要建立在有良好的测试覆盖率底下所运行的,而且在重构之後,程序的行为不应该有一丝一毫的改变,在确定重构完成之後,跑过完所有单元测试通过,再来新增新功能来改变程序的行为。
经过以上的说明,相信读者应该都已经理解接下来我要做的事不是重构(Refactoring),而是重新架构(Re-architect)。原因是因为这个专案到目前为止也没有写过任何的单元测试,所以我没办法保证改完程序码之後,所有的行为会维持的一模一样。虽然说是这样说,但我们还是要尽最大的努力来保持程序码的运作机制来是一样的。
其实这个名词只是为了跟之後要介绍的 Domain Driven Design 要做出比较不一样的区别,上 Google 查了一下才发现原来业界已经有了这个词了,为了避免误人子弟,说我乱用名词,我这边做了一个比较不一样的区别,就是我的 UseCase 是两个字合在一起的,业界的名词是 "Use case" ,两个单字是分开的(根本是在硬凹XD)。
好了,名词都不是重点,这边要示范的是,用前一篇所介绍的 Clean architecture ,大家所认识的 UseCase 为核心所设计出来的架构会是长怎样,所以我才会称为这是 UseCase Driven Design。
目前所遇到第三阶段的需求是,想要能够放大、缩小整个白板,一个萤幕上显示的便利贴数量增加了,因此继续使用之前的做法会有很大的效能负担:
val allNotes: Observable<List<Note>> = noteRepository.getAllNotes()
// 就算是只有一个便利贴往左位移了一个单位,还是会产生一整个 List 的资料给 View 去显示。
那我们能怎麽解决这个效能问题呢?首先我们来看看我们是怎麽使用这 App 的:
以上这些描述,其实就是便利贴这个 App 的 Use case ,在这边我想问一下各位读者:请问读者在看这些 Use case 的时候,有看到描述两个以上便利贴的 Use case 吗?好像没有,对吧?那我们为什麽要一直使用 List 这个资料结构来显示所有的便利贴呢?答案也很简单,因为需要显示在可见范围中的所有便利贴,於是我们有另外一个 Use case:
现在再换个角度想想,如果我们一直都在讨论“便利贴”,而不是“便利贴们”的话,为什麽不让他们各自管理呢?便利贴也有自己的 ViewModel ,这样一来,不管在哪一个便利贴中的哪个栏位被更新了,其他便利贴都不会受到影响不是吗?像下图这种感觉:
在这种情况下,我们是可以完成第四个 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 就出现了:
// 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 ,所以综合分析下来,架构图会是长的像下面这样:
完成了设计之後我们先不要急着实作,可以先尝试其他更多不同的可能性,接着比较不同可能性的优缺点,最後再下手实作,明天将会跟大家介绍 Domain Driven Design 如何在这个 App 派上用场!
>>: [ 卡卡 DAY 13 ] - React Native 页面导览 Navigation (上)
前言 在现实生活中,常见的尺寸单位有公分(cm)、公尺(m)、奈米(nm), 而在网页画面中自己的尺...
话就不多说了,直接开始今天的内容吧 闭包 闭包的别称为「匿名函数」有三个特点 可以像函数一样被呼叫 ...
3-8 前往农场前夕 「设定的方法有很多种,如果是已经知道群数的话,就可以设定k为该群数,让k-me...
Golang OAuth 2.0 在一开始的开赛目标就是希望可以完成golang + OAuth 2...
云端交易主机 - Ubuntu SSH登入 & 远端桌面 SSH登入 本机端建立SSH金钥 ...