𝚲

洋葱架构与类型泛化

看待事物的角度决定了我的认知。

名为洋葱的类型定义

接上回,既然要用 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 表示无类型,相当于很多语言里的 voidMutation 则是一个二元组类型的别名。

具体来讲,解答何时,就是解答:

何地

回答这个问题最容易,肯定是 注入 到某个 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泛化 掉了?这抽象的颗粒度明显不一致啊。

不急,我从两个角度来解释。首先,下面不是还有 PerformanceMeterPerBeat 吗? 前者表示,速心比(英译 MeterPerBeat)是从 A 计算得到;后者,则交代了速心比的数值类型 3。后者看上去是多此一举,但在语义上却是点睛之笔。此时,在概念上,模型对领域的体现是充分的。

其次,A 是刻意留白的 4。此前实现的经验中,我清楚的认识到 Garmin 提供的数据是非常丰富的,如何取舍,我自己也没拿定,后续变化的可能性极大。但无论怎样的变化,Performance 的定义依旧会把焦点拉回到 速心比 上。

解与构

上面的模型定义有没有太过抽象,真的能够指导代码具体实现吗?

不能,确实不能。因为,这样的模型从来不是为了指导代码的具体实现的!它是用来指导 代码是如何拆分,而后又如何构成 的。

软件开发过程中,最难缠的永远不是代码如何实现,而是如何拆分。系统的复杂度越高,拆分的难度越大。而拆分的方案又直接影响到实际开发效率,从分工协同,到集成联调,再到维护扩展。

大道理都对,仍旧空口无凭。好,我们不妨继续看以上模型是怎么体现代码的 解与构

接口与组件

以上 ObserveCallbackInject 可以视为接口, 分别被 domgarminplotly 三个模块实现。其中 dom 需要依赖 Callback 完成变化发生后的回调触发;garmin 需要依赖 Inject 实现数据绘图后的注入。

相应的代码则大致如下:

package dom
given observe(
  using Callback
): Observe = ???
package garmin
given callback(
  using Inject[Data]
): Callback = ???
package plotly
given injectData: Inject[Data] = ???

givenusing ,是 Scala 3 提供的新的语法关键字,作用相当于声明 依赖注入 。值得注意的是,这里的依赖注入是编译期完成的,可理解为是一种语法糖。

而最后集成代码的则是:

import core.Observe
import dom.given
import garmin.given
import ploty.given

def main(args: String*): Unit = 
  summon[Observe](???)

在入口函数 main 中,导入各个组件的 given ,召唤并应用 Observe 函数即可。

到此为止……了吗?若只是为了实现功能,先前的原型就足够了。用 Scala 费这番周折只是一个良好的开端,试想有一天,

至于,洋葱架构与类型泛化,请参见脚注吧 7 8

以上个人观点,不一定对,也可能错的离谱,兼听则明,待续回见。


  1. https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver↩︎

  2. https://en.wikipedia.org/wiki/Observer_pattern↩︎

  3. 更多速心比的话题,请移步 为什么我开始看重跑步时的心率↩︎

  4. “留白”在这里的表达,可能过于含蓄。其本意就是,给后续代码变化预留扩展点。↩︎

  5. https://plotly.com/javascript/↩︎

  6. https://cn.coros.com/↩︎

  7. https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/↩︎

  8. https://en.wikipedia.org/wiki/Generic_programming↩︎