淺聊 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