建造者模式匹配与 2001 年类似

2025-06-08

建造者模式

像2001年一样的配对

像2001年一样的配对

我们将帮助蛋糕店的约翰·麦克雷找到他梦中那个略带讽刺、又奇特又具体的女子。他是个特别的男人:

是的,你我都这么认为:这听起来像是 Rust 编译器的工作。这支乐队确实走在了时代的前列。让我们来模拟一下这个问题:

/// Girl type
struct Girl {}

impl Girl {
    /// Construct a Girl
    fn new() -> Self {
        Self {}
    }
}

/// Determine whether given girl matches spec
fn is_dream_girl(girl: &Girl) -> bool {
    // we don't know anything about spec yet, so odds are no
    false
}

fn main() {
    let girl = Girl::new();
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

运行此程序cargo run将产生预期的输出:Match: false

那么,我们具体要找的是什么呢?幸运的是,我们的男士一开始就告诉我们他的喜好,第一行就告诉我们他想要“一个头脑像钻石一样的女孩”。让我们添加一个会员字段来测试一下:

#[derive(Clone, Copy, PartialEq)]
enum Mind {
    Computer,
    Diamond,
    Garden,
    Scalpel,
    Sieve,
    Unknown,
}

struct Girl {
    mind: Mind,
}

impl Girl {
    fn new(mind: Mind) -> Self {
        Self { mind }
    }
}
Enter fullscreen mode Exit fullscreen mode

现在测试函数可以检查所请求的变体:

fn is_dream_girl(girl: &Girl) -> bool {
    girl.mind == Mind::Diamond
}

fn main() {
    let girl = Girl::new(Mind::Diamond);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们Match: true传入 this 时就得到了结果Girl。不过等等——我们还有一些其他的条件。接下来,我们需要“一个知道什么是最好的女孩”。这很简单——她要么知道,要么不知道:

struct Girl {
    mind: Mind,
    knows_best: bool,
}

impl Girl {
    fn new(mind: Mind, knows_best: bool) -> Self {
        Self { mind, knows_best }
    }
}

fn is_dream_girl(girl: &Girl) -> bool {
    girl.mind == Mind::Diamond && girl.knows_best
}
Enter fullscreen mode Exit fullscreen mode

只需将其添加到参数列表中:

fn main() {
    let girl = Girl::new(Mind::Diamond, true);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们需要“鞋子会割伤,眼睛会像香烟一样燃烧”。听起来我们需要关联几对字符串:

type Attribute = (String, String);

/// Girl type
struct Girl {
    items: Vec<Attribute>,
    mind: Mind,
    knows_best: bool,
}
Enter fullscreen mode Exit fullscreen mode

属性将是一个类似 的元组("shoes", "cut")。我们可以在构造函数中获取 shoes 和 eye 属性:

impl Girl {
    fn new(mind: Mind, knows_best: bool, shoes: &str, eyes: &str) -> Self {
        let mut ret = Self {
            items: Vec::new(),
            mind,
            knows_best,
        };
        ret.push_item("shoes", shoes);
        ret.push_item("eyes", eyes);
        ret
    }

    fn push_item(&mut self, item_name: &str, attribute: &str) {
        self.items.push((item_name.into(), attribute.into()));
    }
}
Enter fullscreen mode Exit fullscreen mode

我们将检查所有物品以确保我们得到了我们想要的东西:

fn is_dream_girl(girl: &Girl) -> bool {
    let mut found_shoes = false;
    let mut found_eyes = false;
    for item in &girl.items {
        if item.0 == "shoes" && item.1 == "cut" {
            found_shoes = true;
        } else if item.0 == "eyes" && item.1 == "burn like cigarettes" {
            found_eyes = true;
        }
    }
    girl.mind == Mind::Diamond && girl.knows_best && found_shoes && found_eyes
}
Enter fullscreen mode Exit fullscreen mode

Girl太棒了!我们只需要用新的属性来构造:

fn main() {
    let girl = Girl::new(Mind::Diamond, true, "cut", "burn like cigarettes");
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

好的。稍等一下。你看出问题了吗?我们先快速浏览一下……

I want a girl with the right allocations
Who's fast and thorough
And sharp as a tack
She's playing with her jewelry
She's putting up her hair
She's touring the facility
And picking up slack
...
Enter fullscreen mode Exit fullscreen mode

一切就从这里继续!这个Girl构造函数已经失控了,我们才勉强完成了第一节。如果约翰改变主意怎么办?他可能会觉得有些事情不那么重要,或者添加一个新的条件。这段代码无法适应这样的变化,每个调用点都依赖于这个以特定顺序给出的参数列表,但人们不会这样做。可能会有各种各样的变化。

模式

让我们利用建造者模式重新实现这个程序。当 aGirl首次构造时,我们只想从一些合理的默认值开始:

struct Girl {
    items: Vec<Attribute>,
    mind: Mind,
    knows_best: bool,
}

impl Girl {
    fn new() -> Self {
        Self::default()
    }
}

impl Default for Girl {
    fn default() -> Self {
        Self { mind: Mind::Unknown, knows_best: false, items: Vec::new() }
    }
}
Enter fullscreen mode Exit fullscreen mode

其他一切都是空白的。这样,我们就可以Girl::new()不带参数直接使用,并得到一个起点。为了添加更多内容,我们可以定义方法:

impl Girl {
    // ..

    fn set_mind(&mut self, mind: Mind) -> &mut Self {
        self.mind = mind;
        self
    }
}
Enter fullscreen mode Exit fullscreen mode

此方法接受一个可变引用并返回一个,因此我们可以先构造然后再调整:

fn main() {
    let mut girl = Girl::new();
    girl.set_mind(Mind::Diamond);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

让我们添加其余部分:

impl Girl {
    fn push_item(&mut self, item_name: &str, attribute: &str) {
        self.items.push((item_name.into(), attribute.into()));
    }

    fn toggle_knows_best(&mut self) -> &mut Self {
        self.knows_best = !self.knows_best;
        self
    }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以一次添加一个,而不管我们在构造时知道什么:

fn main() {
    let mut girl = Girl::new();
    girl.set_mind(Mind::Diamond);
    girl.toggle_knows_best();
    girl.push_item("shoes", "cut");
    girl.push_item("eyes", "burn like cigarettes");
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

随着规范的增长和发展,这将变得更加容易使用。

更复杂的场景可能需要你使用单独的类型,例如 GirlBuilder,并在每一步都拥有所有权。这样你就可以在一行代码中完成所有操作:let girl = GirlBuilder::new().set_mind(Mind::Diamond).toggle_knows_best();但这确实限制了你的配置选项,例如,如果你想在表达式中有条件地调用某个构建器方法if。如果可能的话,这里的非拥有模式会更灵活。

希望我们能够帮助麦克雷先生在这么长时间之后最终安定下来。

挑战

做好准备,五年后这首歌就成为一首“老歌”了。

鏂囩珷鏉ユ簮锛�https://dev.to/decidously/the-builder-pattern-249l
PREV
Web应用程序开发 如何开发Web应用程序? Web应用程序开发流程
NEXT
设置一个新的 Ruby 项目