建造者模式(Builder)
概述
构建者模式是一种设计模式,提供一种灵活的解决方案,已解决面向对象程序设计中的各种对象创建问题。Builder设计模式的目的是将复杂对象的构造与其表示分离开来。是"是*"设计模式之一[wiki]。建造者模式是一种创建型设计模式,使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。
定义:Builder设计模式的目的是将复杂对象的构造与其表示分离开来。通过这样做,同样的构造过程可以创建不同的表示。
历史
假如有一个复杂的对象,需要对其进行构造时需要对诸多成员变量和嵌套对象进行繁杂的初始化工作。有时这些初始化代码通常深藏于一个包含众多参数且让人看不懂的构造函数中;或者这些代码散落在客户端代码的多个位置。
- 例如,创建一个房子,不同种类的房子有不同的风格,为每一种类型的房子创建一个子类,这可能会导致程序变得过于复杂。
- 或者无需生成子类,但是需要创建一个包括所有可能参数的超级构造函数,并用它来控制房屋对象的创建。这样虽然可以避免生成子类,但是会造成当拥有大量输入参数的构造函数不是每次都要全部用上。通常情况下,绝大部分的参数都没有使用,这使得对于构造函数的调用十分不简洁。
建造者模式 的使用
建造者模式建议将对象构造的代码从产品类中抽取出来,并将其放在一个名为生成器的独立对象中。每次创建对象时,都需要通过生成器对象执行一系列步骤。重点在于无需调用所有步骤,而只需调用创建特定对象配置所需的那些步骤。
适用场景
- 使用建造者设计模式可以避免“重叠构造函数”的出现。
- 假设复杂函数中有十几个可选参数,那么调用这些函数会非常不方便,因此需要重载这个构造函数,新建几个只有较少参数的简化版本。
- 建造者设计模式让你可以分步骤生成对象,而且允许你仅适用必须的步骤。
- 当使用代码创建不同形式的产品时,可使用生成器模式
- 如果你需要创建各种形式的产品,他们的制造过程相似且仅有细节上的差异,此时可使用生成器模式。
- 基本生成器接口中定义了所有可能的制造步骤,具体生成器将实现这些步骤来制造特定形式的产品。
- 使用构造者模式构造其他复杂对象
- 构造者模式让你能分步骤构造产品,你可以延迟执行某些步骤而不会影响最终产品。
优点
- 可以分步骤创建对象,暂缓创建步骤或者递归运行创建步骤。
- 生成不同形式的产品,你可以复用相同的制造代码
- 单一职责原则,可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点
由于该模式需要新增多个类,因此代码整体复杂程度会有所增加。
描述
通过使用构建者助手创建一个对象。
例子
fn main() {
let foo = Foo {
bar: String::from("Y"),
};
let foo_from_builder = FooBuilder::new().name(String::from("Y")).build();
println!("foo = {:?}", foo);
println!("foo from builfer = {:?}", foo_from_builder);
}
#[derive(Debug, PartialEq)]
pub struct Foo {
// lots of complicated fields
bar : String,
}
pub struct FooBuilder {
// Probably lots of optional fields.
bar: String,
}
impl FooBuilder {
pub fn new() -> Self {
// set the minimally required fields of Foo.
Self {
bar: String::from("x"),
}
}
pub fn name(mut self, bar: String) -> FooBuilder {
// set the name on the builder iteself,
// and return the builder by value.
self.bar = bar;
self
}
// if we can get away with not consuming the builder here, that is an
// advantage. It means we can use the FooBuilder as a template for constructing many Foo.
pub fn build(self) -> Foo {
// Create a Foo from Foo the FooBuilder, applying all settings in FooBuilder to Foo.
Foo { bar: self.bar }
}
}
// Rust 编程之道. P234
struct Circle {
x: f64,
y: f64,
radius: f64,
}
struct CircleBuilder {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
fn new() -> CircleBuilder {
CircleBuilder {
x: 0.0, y: 0.0, radius: 1.0,
}
}
}
impl CircleBuilder {
fn x(&mut self, coordinate: f64) -> &mut CircleBuilder {
self.x = coordinate;
self
}
fn y(&mut self, coordinate: f64) -> &mut CircleBuilder {
self.y = coordinate;
self
}
fn radius(&mut self, radius: f64) -> &mut CircleBuilder {
self.radius = radius;
self
}
fn build(&self) -> Circle {
Circle {
x: self.x, y: self.y, radius: self.radius,
}
}
}
fn main() {
let c = Circle::new().x(1.0).y(2.0).radius(2.0).build();
println!("area = {:?}", c.area());
println!("c.x = {:?}", c.x);
println!("c.y = {:?}", c.y);
}
动机
当你需要许多不同的构造函数或者当构造有副作用时,这种方法有用。
优点
将构造方法与其他方法分离。
防止构造函数的扩散
可用于单次初始化以及更加复杂的构造。
缺点
比直接创建结构对象或简单的的构造函数更复杂。
讨论
这种模式在Rust(以及简单对象)中比在其他许多语言中更常见,这是因为Rust缺乏重载。由于你只能使用给定名称的单个方法,因此在Rust中使用多个构造函数要比C++、Java或其他语言好。
这种模式通常用于构建器对象本身就很有用的地方,而不仅仅是一个构建器。例如:std::process::Command 是Child的构建器。在这种情况下,不使用T和TBuilder的命名模式。
该示例通过值获取并返回生成器。接受并返回构建器作为可变引用通常更符合人体工程学(并且更有效)。
let mut fb = FooBuilder::new();
fb.a();
fb.b();
let f = fb.builder();
以及FooBuilder::new().a().b().builder()样式。