Rust 中的構造器 - 2 自動生成構造器

類似於 Clone 和 Debug 的工作方式,crates 也可以創建自己的派生宏,有許多 crates 可以幫助我們生成構造器,可以查看#builder(https://lib.rs/keywords/builder)。讓我們來看看其中的一部分:

derive_builder

#[derive(Debug, derive_builder::Builder)]
#[builder(build_fn(validate = ["Self::validate"]))]
struct Query {
    fields: Vec<String>,
    text_filter: String,
    database: String,
    table: String,
    fixed_amount: Option<usize>,
    descending: bool,
}
// Usage same as described patterns:
let query = Query::builder()
    .table("...".into())
    // ...
    .build()
    .unwrap();

這個派生宏生成一個新的 struct,其名稱是在原結構體名字後面加了 Builder(本例中爲 QueryBuilder)。

derive_builder 的缺點是對整個對象進行驗證,而不是對每個字段進行驗證。構造的錯誤變體是 String,這使得匹配錯誤或返回錯誤數據比使用錯誤枚舉更難:

impl Query {
    fn validate(&self) -> Result<(), String> {
        let valid = self
            .database
            .as_ref()
            .map(|value| value == "pg_roles")
            .unwrap_or_default();
        if valid {
            Ok(())
        } else {
            Err("Cannot construct Query on 'pg_roles'".into())
        }
    }
}

typed-builder

Typed-builder 解決了 derive_builder 的問題。

使用 derive_builder,你可以設置一個字段兩次 (或多次):

Query::builder()
    .database("imdb".into())
    // ...
    .database("fishbase".into())

它取最後一個 set 字段的值,它的缺點是擴展宏需要更長的時間,因爲要生成更多的宏。增加的複雜性也使構造器變得更加複雜。

Buildstructor

Buildstructor 是在 impl 塊上的註釋。它不是使用 struct 上的字段 (如前兩個所示) 來生成代碼,而是包裝現有的構造函數:

struct MyStruct {
    sum: usize
}
#[buildstructor::buildstructor]
impl MyStruct {
    #[builder]
    fn new(a: usize, b: usize) -> MyStruct {
        Self { sum: a + b }
    }
}
MyStruct::builder().a(1).b(2).build();

與 type -builder 類似,它生成了構造器的中間 struct,其好處是在編譯時檢查所有字段是否存在。然而,這樣做的缺點是編譯時間較慢,傳遞時靈活性較差。

BuilderStructor 看起來與 Rust 語言更加兼容,允許它支持異步構建器!

其他構建方式

如果你只想構建一個有大量默認字段的 struct,使用..(基本語法) 和 Default trait(無論是自定義實現還是使用 #[derive(Default)] 的默認實現):

#[derive(Default)]
struct X {
    a: u32,
    b: i32,
    c: bool,
}
X { a: 10, ..Default::default() }

如果你想要計算、約束、封裝和命名字段,你可以創建一箇中間 struct,它可以傳遞給構造函數:

struct Report {
    title: String,
    on: chrono[object Object]DateTime
    // ...
}
struct ReportArguments {
    title: String,
    on: Option<chrono::DateTime>
    // ...
}
impl Report {
    fn new_from_arguments(ReportArguments { title, on }: ReportArguments) -> Result<Self, &str> {
        if title.
            .chars()
            .all(|chr| matches!(chr, 'a'..='z' | '0'..='9'))
        {
            Ok(Self {
                title,
                on: chrono.unwrap_or_else(|| todo!())
            })
        } else {
            Err("Invalid report name")
        }
    }
}

然而,這兩個都沒有使用良好的鏈式調用語法。

總結

構造器模式可以幫助我們編寫更清晰、更易讀的 api,它還可以幫助 api 的使用者編寫更好的代碼。我們可以應用約束來確保我們的 struct 被正確初始化,使用一個乾淨的 API 強制執行契約。

本文翻譯自:

https://www.shuttle.rs/blog/2022/06/09/the-builder-pattern

coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/x7JU9ikj-KSXjzvu4A3rvQ