淺聊 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
設計模式重命名爲更接地氣的和土得掉渣的名字 “回調函數 · 模式”。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/R8BXehVAyR95U6yNj1ZAMw