浅聊Rust
【策略·设计模式】Strategy / Policy design pattern
【Rust - Strategy / Policy
策略·模式】与【OOP - Dependency Inversion
依赖倒置·模式】和【Javascript - Callback Functon
回调函数·模式】皆同属一类设计模式组合Inversion of Control Dependency Injection
(控制反转 依赖注入)。为了描述简洁,后文将该组合记作:IoC DI
。
先上图(一图抵千词)
就着上图,我再进一步展开论述。
IoC
容器
在IoC
容器内定义
- “业务总线”。即,算法实现的关键路线·工作流
workflow
。- 在上图中,它就是从【固化模块1】至【固化模块3】的棕色箭头线路·处理流程。
- 可复用模块 — 它既屏蔽了算法的敏感技术细节,也起到了程序复用作用。
- 在上图中,我将其称为“固化模块”。
一般IoC
容器会对外导出一个pub
函数来
- 接收·依赖注入
- 触发执行·整个工作流
DI
依赖注入
利用DI
从“业务总线”上扣出可·填入·自定义实现细节的“trait
坑位” — 非具体类型,避免IoC
容器和单一类型“捆绑”。
- 在上图中,其对应于【可替换模块1】与【待实现模块1】。
作为“坑位”,有两个特质不能少:
- 第一,坑位·填充标准 — 即,坑位的规格。只有满足指定“填充标准”的
struct
实例才被允许注入IoC
容器内的“坑位”里。- 【静态分派】泛型·类型
impl Trait
- 【堆·动态分派】
Box<dyn Trait>
— 允许将【依赖项·构造】业务逻辑抽象至一个独立的【构造函数】内。 - 【栈·动态分派】
&dyn Trait
— 【依赖项·构造】代码必须与【依赖·注入】程序处于同一个函数内,而不能再被抽离·封装于一个独立【构造函数】了。因为没有【所有权·智能指针】保持所有权“不灭”,所以【胖指针】背后的实际变量值会随着【构造函数】的结束执行而被释放掉 — 这会给【构造函数】调用端造成【野指针】困扰,借入检查器是不会答应的。若不明白的话,你再体会,体会! - 在
rust
中,由trait
书面定义“填充·标准”。而且,因为rust
区分【编译时·抽象】与【运行时·抽象】,所以“坑位·规格”又进一步分为: - 在
OOP
中,由interface
书面约定“填充·标准”。 - 因为
js
是弱类型的,所以不需要“书面的”坑位规格描述,开发者把【回调函数】约定记在心里或写到代码注释里即好。
- 【静态分派】泛型·类型
- 第二,坑位·填充物。简单地讲,其就是各种【接口】的实现类·实例。
- 【静态分派】
trait
具体·实现类·实例 — 瘦指针。编译器会自动将【泛型·类型·参数】的【具体·类型】实参展开 — 这叫单态化。 - 【动态分派】
trait Object
— 胖指针。而trait Object
实例是被保存在【栈】上,还是被存储于【堆】内,并不重要。 - 在
rust
中,还是区分【编译时·抽象】与【运行时·抽象】两种情况 - 在
OOP
中,就是实现了interface
的class
实例。 - 在
js
中,就是满足了(你在代码注释里备注的)函数签名约定的回调函数。
- 【静态分派】
trait
坑位
就IoC
容器而言,仅有trait
定义里的
- 成员方法
- 关联函数
- 关联常量
- 关联类型
是可见的。另外,因为rust
允许为trait method
提供默认实现,所以trait
坑位也能为自己提供缺省实现项,若调用端·程序员没有注入定制解决方案的话。
trait
坑位·填充物
首先,在Rust
语境中,该“填充物”有一个专属名词叫作Strategy Structs
。
其次,【闭包Closure
】与【函数指针fn
】被允许经由DI
接口·注入至IoC
容器内·不是什么语言“特例”,而是仅只因为【闭包Closure
】与【函数指针fn
】本质上就是实现了Fn / FnMut / FnOnce trait
的struct
实例。至于它们在字面量上不像struct
,那是因为语法糖:
- 就【闭包】而言,编译器会自动为【闭包】生成一个匿名的
struct
类型,并将被捕获变量作为该struct
类型的(私有)字段。此外,因为每个【闭包】的上下文环境与捕获变量都是不同的,所以每个【闭包】也都有专属的、一个独一无二的匿名struct
类型和不同的私有字段。而在【闭包】体内定义的业务代码则会被封装于【闭包】struct
的Fn::call(&self, args: Args) -> FnOnce::Output
成员方法里。 - 就【函数指针
fn
】而言,fn
自身就是一个无字段的Fn trait
实现类。于是,因为fn
类型没有字段,所以【函数】也就不能捕获任何的外部变量。
编译器真的为我们做了许多的事情。
最后,凭借trait
实现类的(私有)字段,还能实现
- 缓存·中间计算结果
- 捕获·容器外状态数据
的功能。
IoC DI
在rust
的技术落地
相对于弱类型的js
,强类型的rust
- 借助
trait method
,约定“回调函数”的函数签名 —js
没有类型,也就不需要书面地声明(回调)函数签名- 所有·技术细节·都以对
IoC
容器透明的方式被封装于此回调函数里。
- 所有·技术细节·都以对
- 借助
trait
实现类的(私有)字段,从IoC
容器外捕获变量 —js
函数的天赋技能之一就是【捕获变量】,所以不用显示地写这类代码。- 这样从
DI
接口注入就不只是功能“行为”,还有(独立于输入数据的)额外状态信息。
- 这样从
相对于玩转【堆】的java
,rust
还允许向IoC
容器注入复杂数据类型的【栈】变量值,而无论该变量值是被【静态分派】还是【动态分派】。
于是,我的总结是在rust
里的IoC DI
的设计模式落地·比js
严谨,比java
灵活。
综合性【例程】将知识点串联起来
该【例程】实现的功能是:
- 载入【源数据】
- 生成【报表】
- 给【报表】生成【数字签名】 — 防止【报表】内容被篡改。
该【例程】代码分成三个子模块。它们分别对应IoC DI
设计模式内的三大构件:
IoC
容器mod ioc_container
和ioc_container::Report
类型。并且,在ioc_container::Report::generate()
关联函数内定义了- 业务总线
- 可复用的功能模块
ioc_container::Report::sign_me()
给【报表】生成【数字签名】。
DI
注入标准(也称trait
坑位规格)mod di_spec
。只有满足了该规格要求的struct
实例或closure
才能被注入到IoC
容器内。在本例中,包括:- 如何获取【源数据】
di_spec::Ingredient
— 这是一个被动态分派的【闭包】签名。 - 如何格式化【源数据】
di_spec::Formatter
— 这是一个待实现的trait
- 如何获取【源数据】
DI
依赖项(也称trait
坑位·填充物)mod di_stuff
。在本例中,包括:- 它输出了可生成【报表·源数据】的闭包。
- 更重要的是,由此高阶函数输出的闭包满足了
di_spec::Ingredient
定义的函数签名。 - 高阶函数
fn data_builder()
。 - 纯文本格式化【源数据】的代码
di_stuff::Text
JSON
格式化【源数据】的代码di_stuff::Json
最后,在main
函数内,依次
- 实例化
DI
依赖项 - 将
DI
依赖项注入IoC
容器 — 就是给ioc_container::Report::generate()
关联函数传参。 - 执行“业务总线”工作流
- 读取【源数据】,
- 格式化【报表】,
- 生成【数字签名】
- 获得一个
Report
结构体实例。其包括了- 报表的文本内容
- 它的数字签名
思路扩展
【条件编译】plus
【策略·设计模式】是一套非常棒的多平台适配方案。即,
- 将【核心业务】中·与平台相关的·功能模块·扣成
trait
坑位。 - 给每个
trait
坑位准备多套·适配不同(交叉编译)目标平台的·Strategy Structs
具体实现。 - 在编译时,根据
rustc --cfg
或cargo --features
命令行参数,(利用#[cfg(...)]
元属性)将恰当的Strategy Struct
(依赖)注入到·封装了核心业务IoC
容器里的trait
坑位内。 - 输出兼容于指定平台的
exe
或dll
文件。
哎呀!怎么越讲,越像serde crate
了。但是,这么设计真是很【优雅】!
结束语
经由【回调函数】将·可定制技术细节·甩出【主函数】是一条比较常见的编程套路。可是,一旦给“土·方子”赋上一个fancy name
,好似一切都变得好高端、好抽象、好难理解!所以,我个人提议:将Rust - Strategy
设计模式重命名为更接地气的和土得掉渣的名字“回调函数·模式”。