洋葱架构与类型泛化
看待事物的角度决定了我的认知。
接上回,既然要用 Scala 进行重写,那必须充分利用它在类型系统上的优势啊。
这一切还得从领域建模入手。回顾一下我的需求,“画一条速心比的曲线”,我愿将其拆解如下三个问题:
- 何时画?
- 何地画?
- 如何画?
何时
在进行原型实现中,我就发现 connect.garmin.cn
采用的是前端渲染,即页面可视内容均由 Javascript
动态生成。这就意味着,画的时机大概要放在页面内容生成完成之后。为了能够感知页面内容的变化,以判断合适的时机,之前的实现用到了
MutationObserver
1
,这是典型观察者模式的应用场景 2。
自然地,这个领域部分的模型如下:
package core
type Observe = HTMLElement => Unit
type Callback = Mutation => Unit
type Mutation = (URL, Seq[HTMLElement])
以上代码看着有点懵的,稍作解释如下,否则可略过。Observe
是一个特定函数类型的别名,=>
左边的是参数类型,右边的是返回值类型。 Unit
表示无类型,相当于很多语言里的 void
。 Mutation
则是一个二元组类型的别名。
具体来讲,解答何时,就是解答:
- 观察谁的变化?
- 如何处理变化?
- 变化都有什么?
何地
回答这个问题最容易,肯定是 注入 到某个
HTMLElement
里。
type Injection[A] = (HTMLElement, A)
其中的 A
是什么,我下面讲。
如何
这个问题是整个领域的关键,但并不代表它的建模会多复杂。不信,你看:
type Inject [A] = Injection[A] => Unit
type Performance [A] = A => MeterPerBeat
type MeterPerBeat = Double
这里的 A
,也是上一节提到的,代表画图所需要的
数据。Inject
意味着,将数据 A
变成曲线,并注入到一个 HTMLElement
内。有没有觉得哪里不对劲?
为什么在 何时
的问题上,我可以明确定义函数的输入输出类型,而到了 如何
却用 A
给 泛化
掉了?这抽象的颗粒度明显不一致啊。
不急,我从两个角度来解释。首先,下面不是还有 Performance
和 MeterPerBeat
吗? 前者表示,速心比(英译
MeterPerBeat)是从 A
计算得到;后者,则交代了速心比的数值类型 3。后者看上去是多此一举,但在语义上却是点睛之笔。此时,在概念上,模型对领域的体现是充分的。
其次,A
是刻意留白的 4。此前实现的经验中,我清楚的认识到
Garmin
提供的数据是非常丰富的,如何取舍,我自己也没拿定,后续变化的可能性极大。但无论怎样的变化,Performance
的定义依旧会把焦点拉回到 速心比 上。
解与构
上面的模型定义有没有太过抽象,真的能够指导代码具体实现吗?
不能,确实不能。因为,这样的模型从来不是为了指导代码的具体实现的!它是用来指导 代码是如何拆分,而后又如何构成 的。
软件开发过程中,最难缠的永远不是代码如何实现,而是如何拆分。系统的复杂度越高,拆分的难度越大。而拆分的方案又直接影响到实际开发效率,从分工协同,到集成联调,再到维护扩展。
大道理都对,仍旧空口无凭。好,我们不妨继续看以上模型是怎么体现代码的 解与构。
以上 Observe
、Callback
、Inject
可以视为接口, 分别被 dom
、garmin
、plotly
三个模块实现。其中
dom
需要依赖 Callback
完成变化发生后的回调触发;garmin
需要依赖
Inject
实现数据绘图后的注入。
相应的代码则大致如下:
package dom
observe(
given Callback
using ): Observe = ???
package garmin
callback(
given [Data]
using Inject): Callback = ???
package plotly
: Inject[Data] = ??? given injectData
given
和 using
,是 Scala 3
提供的新的语法关键字,作用相当于声明 依赖注入
。值得注意的是,这里的依赖注入是编译期完成的,可理解为是一种语法糖。
而最后集成代码的则是:
import core.Observe
import dom.given
import garmin.given
import ploty.given
def main(args: String*): Unit =
[Observe](???) summon
在入口函数 main
中,导入各个组件的 given
,召唤并应用 Observe
函数即可。
到此为止……了吗?若只是为了实现功能,先前的原型就足够了。用 Scala 费这番周折只是一个良好的开端,试想有一天,
以上个人观点,不一定对,也可能错的离谱,兼听则明,待续回见。
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver↩︎
更多速心比的话题,请移步 为什么我开始看重跑步时的心率 。↩︎
“留白”在这里的表达,可能过于含蓄。其本意就是,给后续代码变化预留扩展点。↩︎
https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/↩︎