浅聊 Rust 【策略·设计模式】 Strategy / Policy design pattern

2022-11-28 14:55:21 浏览数 (1)

浅聊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中,就是实现了interfaceclass实例。
    • js中,就是满足了(你在代码注释里备注的)函数签名约定的回调函数。

trait坑位

IoC容器而言,仅有trait定义里的

  • 成员方法
  • 关联函数
  • 关联常量
  • 关联类型

是可见的。另外,因为rust允许为trait method提供默认实现,所以trait坑位也能为自己提供缺省实现项,若调用端·程序员没有注入定制解决方案的话。

trait坑位·填充物

首先,在Rust语境中,该“填充物”有一个专属名词叫作Strategy Structs

其次,【闭包Closure】与【函数指针fn】被允许经由DI接口·注入至IoC容器内·不是什么语言“特例”,而是仅只因为【闭包Closure】与【函数指针fn】本质上就是实现了Fn / FnMut / FnOnce traitstruct实例。至于它们在字面量上不像struct,那是因为语法糖:

  • 就【闭包】而言,编译器会自动为【闭包】生成一个匿名的struct类型,并将被捕获变量作为该struct类型的(私有)字段。此外,因为每个【闭包】的上下文环境与捕获变量都是不同的,所以每个【闭包】也都有专属的、一个独一无二的匿名struct类型和不同的私有字段。而在【闭包】体内定义的业务代码则会被封装于【闭包】structFn::call(&self, args: Args) -> FnOnce::Output成员方法里。
  • 就【函数指针fn】而言,fn自身就是一个无字段的Fn trait实现类。于是,因为fn类型没有字段,所以【函数】也就不能捕获任何的外部变量。

编译器真的为我们做了许多的事情。

最后,凭借trait实现类的(私有)字段,还能实现

  • 缓存·中间计算结果
  • 捕获·容器外状态数据

的功能。

IoC DIrust的技术落地

相对于弱类型的js,强类型的rust

  • 借助trait method,约定“回调函数”的函数签名 — js没有类型,也就不需要书面地声明(回调)函数签名
    • 所有·技术细节·都以对IoC容器透明的方式被封装于此回调函数里。
  • 借助trait实现类的(私有)字段,从IoC容器外捕获变量 — js函数的天赋技能之一就是【捕获变量】,所以不用显示地写这类代码。
    • 这样从DI接口注入就不只是功能“行为”,还有(独立于输入数据的)额外状态信息。

相对于玩转【堆】的javarust还允许向IoC容器注入复杂数据类型的【栈】变量值,而无论该变量值是被【静态分派】还是【动态分派】。

于是,我的总结是在rust里的IoC DI的设计模式落地·比js严谨,比java灵活。

综合性【例程】将知识点串联起来

该【例程】实现的功能是:

  • 载入【源数据】
  • 生成【报表】
  • 给【报表】生成【数字签名】 — 防止【报表】内容被篡改。

该【例程】代码分成三个子模块。它们分别对应IoC DI设计模式内的三大构件:

  1. IoC容器mod ioc_containerioc_container::Report类型。并且,在ioc_container::Report::generate()关联函数内定义了
    1. 业务总线
    2. 可复用的功能模块ioc_container::Report::sign_me()给【报表】生成【数字签名】。
  2. DI注入标准(也称trait坑位规格)mod di_spec。只有满足了该规格要求的struct实例或closure才能被注入到IoC容器内。在本例中,包括:
    1. 如何获取【源数据】di_spec::Ingredient— 这是一个被动态分派的【闭包】签名。
    2. 如何格式化【源数据】di_spec::Formatter — 这是一个待实现的trait
  3. DI依赖项(也称trait坑位·填充物)mod di_stuff。在本例中,包括:
    1. 它输出了可生成【报表·源数据】的闭包。
    2. 更重要的是,由此高阶函数输出的闭包满足了di_spec::Ingredient定义的函数签名。
    3. 高阶函数fn data_builder()
    4. 纯文本格式化【源数据】的代码di_stuff::Text
    5. JSON格式化【源数据】的代码di_stuff::Json

最后,在main函数内,依次

  1. 实例化DI依赖项
  2. DI依赖项注入IoC容器 — 就是给ioc_container::Report::generate()关联函数传参。
  3. 执行“业务总线”工作流
    1. 读取【源数据】,
    2. 格式化【报表】,
    3. 生成【数字签名】
  4. 获得一个Report结构体实例。其包括了
    1. 报表的文本内容
    2. 它的数字签名

思路扩展

【条件编译】plus【策略·设计模式】是一套非常棒的多平台适配方案。即,

  1. 将【核心业务】中·与平台相关的·功能模块·扣成trait坑位。
  2. 给每个trait坑位准备多套·适配不同(交叉编译)目标平台的·Strategy Structs具体实现。
  3. 在编译时,根据rustc --cfgcargo --features命令行参数,(利用#[cfg(...)]元属性)将恰当的Strategy Struct(依赖)注入到·封装了核心业务IoC容器里的trait坑位内。
  4. 输出兼容于指定平台的exedll文件。

哎呀!怎么越讲,越像serde crate了。但是,这么设计真是很【优雅】!

结束语

经由【回调函数】将·可定制技术细节·甩出【主函数】是一条比较常见的编程套路。可是,一旦给“土·方子”赋上一个fancy name,好似一切都变得好高端、好抽象、好难理解!所以,我个人提议:将Rust - Strategy设计模式重命名为更接地气的和土得掉渣的名字“回调函数·模式”。

0 人点赞