上一次介绍完了介面,今天就要来说说实作的部分了,从这里开始我要采取一种“小步快跑”的方式,原本 EditorViewModel
负责处理所有的商业逻辑,现在将原本的做的事情慢慢减少,将部分的职责一个一个的交给 CoEditor
,然後每次完成之後再建置、执行、测试、commit,确定没问题了之後再进行下一次的迭代。
class EditorViewModel(
private val coEditor: CoEditor,
private val noteRepository: NoteRepository //[1]
): ViewModel() {
init {
coEditor.start() // [2]
}
fun moveNote(noteId: String, positionDelta: Position) {
coEditor.moveNote(noteId, positionDelta) // [3]
}
fun addNewNote() {
coEditor.addNewNote() // [3]
}
// ..others
override fun onCleared() {
coEditor.stop() // [4]
disposableBag.clear()
}
}
class CoEditor(private val noteRepository: NoteRepository) {
// 从 EditorViewModel 搬过来的
fun addNewNote() {
val newNote = Note.createRandomNote()
noteRepository.addNote(newNote)
}
fun moveNote(noteId: String, positionDelta: Position) {
Observable.just(positionDelta)
.withLatestFrom(noteRepository.getNoteById(noteId)) { delta, note ->
note.copy(position = note.position + delta)
}
.subscribe { note ->
noteRepository.putNote(note)
}
.addTo(disposableBag)
}
}
首先请看 [3] , addNewNote
以及 moveNote
,这两个函式是最好实作的,因为他们可以透过 NoteRepository
直接去做呼叫,所以直接把所有程序码从 EditorViewModel
搬过去即可。与此同时,在 re-architect 的过程中,我们没有想要一步到位,所以在建构子 [1] 这边暂时会有 NoteRepository
与 CoEditro
这两个相依的类别,等到 re-architect 完成後, 会将 NoteRepository
拿掉。最後是 [2] 跟 [4] ,因为 CoEditro
是一个拥有生命周期的元件,所以必须要在对的地方呼叫 start 跟 stop,否则在 CoEditro
中的 Reactive stream 会因为没有正确的回收而造成 memory leak。
以上完成了之後,建置、执行、测试都没问题,因此可以放心的进行下一步:
class EditorViewModel(
private val coEditor: CoEditor,
private val noteRepository: NoteRepository
): ViewModel() {
val selectingNote: Observable<Optional<Note>> = coEditor.selectedNote
fun tapNote(note: Note) {
coEditor.selectNote(note.id)
}
fun tapCanvas() {
coEditor.clearSelection()
}
// ..others
}
class CoEditor(private val noteRepository: NoteRepository) {
// 这边基本上也是从 EditorViewModel 搬过来,只有做点小修改
private val selectedNoteId = BehaviorSubject.createDefault(Optional.empty<String>())
val selectedNote: Observable<Optional<Note>> = selectedNoteId
.flatMap { optId ->
if (optId.isPresent) {
noteRepository.getNoteById(optId.get())
.map { Optional.ofNullable(it) }
} else {
Observable.just(Optional.empty())
}
}
fun selectNote(noteId: String) {
selectedNoteId.onNext(Optional.of(noteId))
}
fun clearSelection() {
selectedNoteId.onNext(Optional.empty())
}
// ..others
}
接下来是选择状态,其实这边的逻辑跟之前也是一样的,只要搬程序码就好,但是比较棘手的是:删除、更改颜色、更改文字,这些功能都需要这个状态的值。这边继续秉持着采用“小步快跑”的精神,不要一次改太多,所以先让这几个功能坏掉,之後再补上即可。
建置、执行、测试之後发现选择状态是 OK 的,这边没问题之後,就来补上删除、更改颜色、更改文字这些功能了:
class EditorViewModel(
private val coEditor: CoEditor,
private val noteRepository: NoteRepository
): ViewModel() {
val selectingColor: Observable<YBColor> = noteEditor.contextMenu.selectedColor
val openEditTextScreen: Observable<String> = noteEditor.openEditTextScreen
fun onDeleteClicked() {
coEditor.contextMenu.onDeleteClicked()
}
fun onColorSelected(color: YBColor) {
coEditor.contextMenu.onColorSelected(color)
}
fun onEditTextClicked() {
coEditor.contextMenu.onEditTextClicked()
}
// ..others
}
由於这些功能都是挂在 contextMenu 底下,所以这边还有再经过一层 coEditor.contextMenu
的呼叫来使用它们。但是, ContextMenu
要怎麽来实作 onDeleteClicked
、onColorSelected
、onEditTextClicked
这些函式呢?事情开始有趣起来了,依我们现在的设计,删除、选择颜色、 编辑文字这些按钮的行为应该都要属於 ContextMenu
,但是现在 ContextMenu
既不认识 selectingNote
也不认识 NoteRepository
,有了 selectingNote
才可以拿到最新的状态去更新,有了 NoteRepository
才有办法更新资料在云端上,那我该让 ContextMenu 认识他们吗?
仔细思考过後,我觉得所有核心的逻辑操作还是放在 CoEditor
比较好,原因是因为他们都跟“共编器”这个概念有相关,全部都放在同一个地方之後要再做重构或整理也比较方便,於是我就想到了一个方案:让 ContextMenu
丢出事件,这些被丢出来的事件会被传送到 CoEditor
,并且让它处理该做的核心商业逻辑运算。
sealed interface ContextMenuEvent {
object NavigateToEditTextPage: ContextMenuEvent
object DeleteNote: ContextMenuEvent
class ChangeColor(val color: YBColor): ContextMenuEvent
}
这些事件只会存在於 Domain 层,由 ContextMenu
传给 CoEditor
,用 PublishSubject 可以轻松的帮我们完成这件事:
class ContextMenu(
private val selectedNote: Observable<Optional<Note>>
) {
private val _contextMenuEvents = PublishSubject.create<ContextMenuEvent>()
val contextMenuEvents: Observable<ContextMenuEvent> = _contextMenuEvents.hide()
fun onColorSelected(color: YBColor) {
_contextMenuEvents.onNext(ContextMenuEvent.ChangeColor(color))
}
fun onDeleteClicked() {
_contextMenuEvents.onNext(ContextMenuEvent.DeleteNote)
}
fun onEditTextClicked() {
_contextMenuEvents.onNext(ContextMenuEvent.NavigateToEditTextPage)
}
}
接着只要在 CoEditor
中绑定 contextMenuEvents 即可:
class CoEditor(private val noteRepository: NoteRepository) {
fun start() {
contextMenu.contextMenuEvents
.subscribe { menuEvent ->
when(menuEvent) {
ContextMenuEvent.NavigateToEditTextPage -> navigateToEditTextPage()
is ContextMenuEvent.ChangeColor -> changeColor(menuEvent.color)
ContextMenuEvent.DeleteNote -> deleteNote()
}
}
.addTo(disposableBag)
}
// ..others
// 相关的实作细节有兴趣的可以去 github 看:https://github.com/hungyanbin/ReactiveStickyNote/blob/DDD_implementation/app/src/main/java/com/yanbin/reactivestickynote/domain/CoEditor.kt
private fun navigateToEditTextPage() { ... }
private fun deleteNote() { ... }
private fun changeColor(color: YBColor) { ... }
}
到这个阶段告一段落,一样建置、执行、测试,一切顺利!
现在还是使用全部的 Note 来显示便利贴的位置以及其他相关状态比较好,还未能实现每个独立的便利贴各自更新,因为如果为了要达到各自更新这个目的,还得要有资料层与显示层的支援才行。以现在状态来说还不是一个适合的时机点,先把搬运程序码的部分告一段落才行,一次做一件事才能好好的把事情做好,所以现在要做的是,暂时的在 CoEditor
上加一个成员变数,用来拿所有的便利贴,当然,这边的实作方式也是跟在 EditorViewModel
是一模一样的:
class EditorViewModel(
private val coEditor: CoEditor,
private val noteRepository: NoteRepository
): ViewModel() {
val allNotes: Observable<List<Note>> = coEditor.allNotes
}
class CoEditor(private val noteRepository: NoteRepository) {
@Deprecated("Will remove later")
val allNotes = noteRepository.getAllNotes()
}
至今为止, EditorViewModel
已经没有任何的商业逻辑了,将核心的商业逻辑全搬到了 CoEditor
与 ContextMenu
,这步完成了之後,我们在未来就可以放心的在 View 层进行较大规模的重组。以下是最後 EditorViewModel
的样子:
class EditorViewModel(
private val coEditor: CoEditor
): ViewModel() {
private val disposableBag = CompositeDisposable()
val allNotes: Observable<List<Note>> = coEditor.allNotes
val selectingNote: Observable<Optional<Note>> = coEditor.selectedNote
val selectingColor: Observable<YBColor> = coEditor.contextMenu.selectedColor
val openEditTextScreen: Observable<String> = coEditor.openEditTextScreen
init {
coEditor.start()
}
fun moveNote(noteId: String, positionDelta: Position) {
coEditor.moveNote(noteId, positionDelta)
}
fun addNewNote() {
coEditor.addNewNote()
}
fun tapNote(note: Note) {
coEditor.selectNote(note.id)
}
fun tapCanvas() {
coEditor.clearSelection()
}
fun onDeleteClicked() {
coEditor.contextMenu.onDeleteClicked()
}
fun onColorSelected(color: YBColor) {
coEditor.contextMenu.onColorSelected(color)
}
fun onEditTextClicked() {
coEditor.contextMenu.onEditTextClicked()
}
override fun onCleared() {
coEditor.stop()
disposableBag.clear()
}
}
今天分享了“小步慢跑”的手法,让程序码随时处於一个可以运行的状态,首先产生一个空壳的 Domain 层物件,从最简单的使用案例开始,慢慢把核心逻辑从 ViewModel 层搬运到了 Domain 层,直到 ViewModel 层的职责几乎完全脱离商业逻辑为止,当然,这时候你可能会觉得我们在浪费时间,只是在左手换右手,但是根据我们之前画的架构图,这个步骤是看得出价值的,因为在之後,这些 Domain 层物件将会各自有属於他们的 ViewModel ,职责会更加的单一,慢慢的去形成属於我们 App 商业行为的形状,而不是过往所看到的,开发人员为了迎合某个框架而变成“框架的形状”。一但 App 的架构与“模型”还有“共通语言”是高度相关的,要增加新功能也只是一块小蛋糕而已,其维护的成本会大大的降低,开发速度会有显着的提升!
注:如果 EditorViewModel
原本有写单元测试的话我们 re-architect 就会更加的放心,但是单元测试并不在这系列文章的范围里,也并不是说单元测试对於架构来说不重要,只是我觉得测试这一块再加进来就有点太多了,我想专注的写好架构以及 RP 这一块,所以单元测试的这部分就靠各位读者自己去研究了。
刚开始看不懂那些乱码时,真的很痛苦。 Regular Expression常简写regexp,也是R...
今天跟大家介绍 Mail 通知,其实就是 SMTP server ~ 首先就是要准备一组帐号密码,通...
前言 昨天已经将 addAlarmTableViewCell 在 addAlarmTableView...
awk Linux文字处理工具中, 有另一个杀器awk 但awk是个程序语言, 所以它很灵活且功能强...
在Wentz Wu网站上说明,CIA是美国法定目标(PUBLIC LAW 107–347—DEC. ...