使用 Domain Driven Design 来进行架构设计

接续上一篇的故事,阿明跟小美又经过了几次的对话与讨论,在便利贴专案中整理归纳了下列这几个关键字:

  • CoEditor
  • ViewPort
  • Gesture
  • StickyNote
  • ContextMenu
  • AdderButton
  • Selection state

有了这几个关键字之後,所有的专案成员便一致认同该进行架构设计的阶段了,但是重新设计架构不是一蹴可几,分几个不同的阶段慢慢来才是更好的做法。首先第一步是为最完整的需求勾勒出大致上的轮廓,确保元件之间的交互关系之後,再选择该架构中实作的优先顺序,其中以满足之前的需求为优先,在确定不会破坏任何行为之後,再将新需求实作进来,最後做成完成品。

元件之间的关系

下图是这几个元件之间的交互关系,这不是类别图,也不是流程图,就只是表达模型的一张图。

Sticky note-5.jpg

  • Gesture 可以控制 CoEditor
  • CoEditor 拥有 ViewPort, ContextMenu, AdderButton
  • ViewPort 拥有许多的 StickyNote

接着我们试看看之前订出来的 Use case 在这样的架构下能不能正确运行吧:

  1. 将手指放到便利贴上面,持续不断的触碰不离开萤幕,该便利贴便会随着手指的位置而跟着移动。

以这个 Use case 来说,是相对难实现的,因为手势操作会从 Gesture 进来,经过 CoEditor、ViewPort,最後才能直接操作到对应的 StickyNote,而且“有没有接触到便利贴”这件事变成要从 Domain 这边来处理,也就是说要依据 StickyNote 的宽跟高还有位置来判断点击事件,做的事情变得有点多,但这其实也隐含了一件事,就是 StickyNote 的宽跟高应该是 Domain 中的一部分,但是到现在都还没有讨论到这一块。为了避免将专案规模越弄越大,这时候最应该要做的事情是将此事与 Stakeholder 讨论,一方面思考较简单的解法。

经过讨论与思考後,因为实作的工程浩大,最终决定使用原本的做法:从外面的 StickyNoteView 操控相对应的便利贴,如果之後有需要的话会再统一由 Gesture 控制。

  1. 点击某张便利贴,开启选择状态,点选选单中的删除按钮,之後就会看到该张便利贴被删掉了。

既然上面都决定了拖曳便利贴行为还是交给每个便利贴自己处理,选择行为也会如法炮制,因此 StickyNote 将会有一个点击行为的 Callback 经由 ViewPort 再交给 CoEditor 处理,CoEditor 将会知道该让哪一个便利贴启用选择状态,并将这选择状态再传递回去给所有的便利贴,以更新选择状态。

与此同时,CoEditor 开启了 ContextMenu ,当按下删除时,由於 CoEditor 已经知道选择的便利贴是哪一个,所以删除就变得简单了。

  1. 点击某张便利贴,开启选择状态,点选选单中的某个颜色,之後就会看到该张便利贴的背景颜色换了。

跟删除的 Use case 是差不多的,所以就不分析了

  1. 在可见范围中,需要能看到所有在范围内便利贴。

这边预计是由 ViewPort 来控制的,当可见范围改变时,可能是执行其中的 move() 或是 zoomIn() 之类的函式,然後可以看到的便利贴数量也会因此增加或减少,因此这元件应该要直接拥有 NoteRepostory 的相依才是。

  1. 在萤幕中显示的便利贴,要反应现在的最新状态并正确显示。

这也是不好解决的问题,但是根据之前在 Clean architecture 分析时学到的,可以将这边的 StickyNote 改成 StickyNoteId 就好,等到该便利贴被 ViewPort 观察到时,再依序建立出 StickyNoteView 还有 StickyNoteViewModel ,最後 StickyNoteViewModel 会与这边的 StickyNote 产生连结而拿到最新的值。

下一步 - Class Diagram

从上一个阶段我们可以得知,其中最核心的元件是 CoEditor ,於是我想采取的第一步是从建立这个类别开始,慢慢把 ViewModel 的实作搬到核心元件中,当这部分成功了之後,就可以再针对 Domain 中各小元件来进行拆分。於是就产生了以下的类别图:

Sticky note-3.jpg

大概解释一下这张图:蓝色的部分是 Domain ,红色的部分是 View ,黑色的部分是 Data 。

首先可以来看看 Domain 的部分,总共有三个不同的元件:CoEditorContextMenuNoteRepository,他们各别的职责就算不用解释也大概知道在做什麽,至於其他的部分,像是 Gesture 与 ViewPort ,就留给之後再做,目前这样的规模要动的程序码已经很多了。

Data 这边没有任何变化,所以就不解说了,但是 View 的变化就很大了,除了原本的 EditorViewModel 将重新命名为 CoEditorViewModel 之外,还有其他两个新的 ViewModel:NoteViewModelContextMenuViewModel ,ViewModel 的定位将会从架构中的核心,转移到 Clean architecture 中的 Port and Adapter,因此将不会预期这些元件会有复杂的领域知识与逻辑,就只是一个 Adapter 而已。其中也可以注意一下箭头的方向,他们都有符合 Dependency rule,箭头是从外层内层。至於其他的,剩下来的就是 Jetpack Compose 实作的 View 层元件:CoEditorScreenViewPortViewNoteViewContextMenuView,他们的关系并不复杂,但是其中有一个最大的变化就是,现在不只有 CoEditorScreen 认识 ViewModel ,其他的 View 各认识他们所应该认识的 ViewModel ,这代表 ContextMenuView, NoteView 这些 View 将会是 Stateful 的,在之前的篇幅中有说过,Stateful 元件的可重用性不高,因此是比较没有那麽推荐去做一个有 Stateful 的 Composable function,至於这边要怎麽处理,在之後将会详细的介绍。

小结

从第 22 天开始,我们做了很多架构的讨论以及设计,当中也产出了一些手绘的架构图,从比较宏观的角度将所有需要的元件写出来,这种设计的方式,叫做 Top-down design,一开始在学习 TDD ,或是一些敏捷开发理论时,会以为这样的开发方式很“不敏捷”,因为必须经过很长的一段时间设计才能得到成果,看起来很像是在做瀑布式开发。但後来发现,这样的设计方式其实跟敏捷一点都不冲突,因为在做“全面”的设计时,并不代表这样的设计是一次到位,不允许被改变的,相对的,如果好好运用敏捷的方式,在经过数个开发循环之後,你将会发现一开始设计的架构还有很多地方需要改进的,这时就是最好的修正机会。然後再进行讨论,微调架构,在下一个循环修正完之後,你会发现你对正在解决的问题有更深层的理解,团队有了更多的共识。


<<:  Day-29 番外篇:放下笨重的变压器、电源线瘦身计画

>>:  Day 18 Sort

JavaScript的语法规定

JavaScript 程序基本上就是编写一连串的指令 (instructions),告诉浏览器要做什...

【Day26】this - 物件的方法调用

在讲解 this 之前,先来看一段程序码,观察它的执行过程 var myName = 'weiwei...

Day25 建立角色功能

首先建立装载角色资料的 ViewModel,因为接下来的权限会以角色判断,ASP.NET Core ...

Unity自主学习(一):认识2D/3D游戏引擎-Unity

动机 决定题目的动机是,从最早开始决定就读资讯相关就想着要在未来踏入电脑游戏产业,而作为许多初学者最...

【PHP Telegram Bot】Day29 - 社群按赞机器人(1):让频道出现按赞按钮

今天来做这个很实用的东东,很多频道都有这个功能 将机器人加入频道 机器人要加入频道的话只能加成管理...