构建者模式:哪个变体更受青睐?

7

我正在阅读《Effective Java》这本书,为了将来的参考而做笔记时,我遇到了建造者模式。

我明白它是什么,以及它应该如何使用。在这个过程中,我创建了两个建造者模式的示例变体。

我需要帮助列出它们之间的区别和优点。很明显,示例1暴露出更少的方法,因此它更加通用和灵活,从而允许更加灵活地使用。

请指出我可能错过的其他事情?

示例1

package item2;

/**
 * @author Sudhakar Duraiswamy
 *
 */
public  class Vehicle {

    private String type;
    private int wheels;

    interface Builder<T>{
        public  T build();
    }

    public static class CarBuilder implements Builder<Vehicle>{
        private String type;
        private int wheels;     

        CarBuilder createVehicle(){
            this.type= "Car";
            return this;
        }

        CarBuilder addWheels(int wheels){
            this.wheels = wheels;
            return this;
        }

        public Vehicle build(){
            Vehicle v = new Vehicle();
            v.type = type;
            v.wheels = wheels;
            return v;
        }               
    }

    public static class TruckBuilder implements Builder<Vehicle>{       
        private String type;
        private int wheels; 

        TruckBuilder createVehicle(){           
            this.type= "Truck";
            return this;
        }

        TruckBuilder addWheels(int wheels){
            this.wheels = wheels;
            return this;
        }

        public Vehicle build(){
            Vehicle v = new Vehicle();
            v.type = type;
            v.wheels = wheels;
            return v;
        }
    }   

    public Vehicle(){

    }

    public static void main(String[] args) {
        //This builds a car with 4 wheels
        Vehicle car = new Vehicle.CarBuilder().createVehicle().addWheels(4).build();

        //THis builds a Truck with 10 wheels
        Vehicle truck = new Vehicle.TruckBuilder().createVehicle().addWheels(10).build();

    }
}

例子2

package item2;
/**
 * @author Sudhakar Duraiswamy
 *
 */
public  class Vehicle2 {

    private String type;
    private int wheels;

    interface Builder<T>{
        public  T build();      
        public String getType();
        public int getWheels() ;
    }

    public static class CarBuilder implements Builder<Vehicle2>{
        private String type;
        private int wheels;     

        public String getType() {
            return type;
        }
        public int getWheels() {
            return wheels;
        }

        CarBuilder createVehicle(){
            this.type= "Car";
            return this;
        }

        CarBuilder addWheels(int wheels){
            this.wheels = wheels;
            return this;
        }

        public Vehicle2 build(){        
            return new Vehicle2(this);
        }               
    }

    public static class TruckBuilder implements Builder<Vehicle2>{      
        private String type;
        private int wheels; 

        public String getType() {
            return type;
        }

        public int getWheels() {
            return wheels;
        }

        TruckBuilder createVehicle(){           
            this.type= "Truck";
            return this;
        }

        TruckBuilder addWheels(int wheels){
            this.wheels = wheels;
            return this;
        }

        public Vehicle2 build(){
            return new Vehicle2(this);
        }
    }


public Vehicle2(Builder<? extends Vehicle2> builder){
    Vehicle2 v = new Vehicle2();
    v.type = builder.getType();
    v.wheels = builder.getWheels();
}

    public Vehicle2(){
    }

    public static void main(String[] args) {            
        //This builds a car with 4 wheels
        Vehicle2 car = new Vehicle2.CarBuilder().createVehicle().addWheels(4).build();

        //THis builds a Truck with 10 wheels
        Vehicle2 truck = new Vehicle2.TruckBuilder().createVehicle().addWheels(10).build();
    }
}

8
不留言就恶意地进行负面投票是很幼稚的行为。 - Sudhakar
3
各位,如果您能留下评论说明为什么这个问题不合适,那真的会对我和其他人有所帮助。 - Sudhakar
你应该浏览这个网站http://en.wikipedia.org/wiki/Builder_pattern#Java,以查看“建造者模式”实现的适当示例。 - Vishal K
2个回答

9

以上都不是。

第一个例子不允许构建不可变的交通工具,这常常是使用 Builder 模式的原因。

第二个例子是第一个例子的变化版本,它允许通过额外的 getter 方法从 builder 中获取信息。但那些方法没有被任何地方使用,除了在 Vehicle 构造函数中,该构造函数直接访问 builder 字段。我不明白添加它们的意义所在。

我认为有两个更重要的改进点:

  1. 这两种 builder 类型完全做同样的事情。没有必要有两种类型,一种就足够了。
  2. createVehicle() 方法的功能应该由 builder 构造函数完成。如果你构造一个 CarBuilder,显然是要构建一辆车,因此车辆类型应该尽早在 builder 构造函数中设置。以下是我的写法:

.

public final class Vehicle {

    private final String type;
    private final int wheels;

    private Vehicle(Builder builder) {
        this.type = builder.type;
        this.wheels = builder.wheels;
    }

    public static Builder carBuilder() {
        return new Builder("car");
    }

    public static Builder truckBuilder() {
        return new Builder("truck");
    }

    public static class Builder {
        private final String type;
        private int wheels;

        private Builder(String type) {
            this.type = type;
        }

        public Builder addWheels(int wheels){
            this.wheels = wheels;
            return this;
        }

        public Vehicle build() {
            return new Vehicle(this);
        }               
    }

    public static void main(String[] args) {
        Vehicle car = Vehicle.carBuilder().addWheels(4).build();
        Vehicle truck = Vehicle.truckBuilder().addWheels(10).build();
    }
}

感谢您指出“不可变性”,我想那就是我搞错了的地方。我原以为建造者模式主要用于隐藏涉及构建复杂对象的步骤,但不可变性并不是强制要求。感谢您提供了一个例子。干杯! - Sudhakar
JB Nizet:如果我在Example 1中的实例变量中引入“final”,那么它会形成一个有效的BuilderPattern,对吗? - Sudhakar
如果你将字段设为final,它就无法编译了,因为你试图从构建器中修改字段的值。你的构建器Builder模式的示例。但第一个构建器不允许不可变性并且有冗余代码,第二个构建器也有冗余代码。它们都可以通过使用我在答案中展示的代码进行改进,这样更加简洁、安全,并且允许不可变性。 - JB Nizet
好的,在你的例子中,Builder 中的 'wheels' 变量仍然是可变的。这使得 Builder 实例可以被重复使用,当然,它会生成一个新的 Vehicle 实例,并且不会以任何方式影响之前创建的 Vehicle。但是这样做还可以吗?你能否澄清一下?Builder 的可重用性是否是正确的实践? - Sudhakar
建造者模式的重点是构建一个不可变的车辆。当然,建造者必须是可变的,否则我们就回到了原点。而不可变性并不意味着对象不能被重用。它意味着其状态在构建时已知且固定,并且在此后永远不会更改。请重新阅读Effective Java中关于不可变性的章节。如果您不希望建造者被重用,请在调用build()方法时设置一个标志,并在再次调用时抛出异常。但这与不可变性无关。 - JB Nizet

3

还有一种更简洁的变体:

建造者不需要拥有自己的实例字段,而是可以改变Vehicle的状态。内部类可以写入其外部类的私有成员:

class Vehicle {
  private int wheels;

  private Vehicle() {}

  public static class Builder {
    private boolean building = true;
    private Vehicle vehicle = new Vehicle();

    public Builder buildWheels(int wheels) {
      if(!this.building) throw new IllegalStateException();
      this.vehicle.wheels = wheels;
      return this;
    }

    public Vehicle build() {
      this.building = false;
      return this.vehicle;
    }
  }
}

由于这些字段是私有的,且只允许构建一次(building标志),即使这些字段不再是final(没有更多的真正的不变性,参见Eric的博客文章,虽然这是关于C#的,但概念相似),但已经构建的Vehicle实例对消费者仍然是不可变的。

需要更加小心,因为非final字段在对象构建过程中不必初始化(由编译器强制执行),你必须仔细检查building状态。但是,你确实可以节省所有实例字段的完整副本。总的来说,如果你有一个相当大的实例变量集,每个方法构造几个字段的情况下,这将非常有用。

我知道这并没有指出你的方法的任何优劣。但是,如果你不需要这些字段是final,那么这种方法可以节省很多额外的代码。


不,Vechicle 实例不是不可变的;请参见此答案:https://dev59.com/zljUa4cB1Zd3GeqPQluI#6388762 - Costi Ciudatu
@CostiCiudatu 感谢您指出;我在最初的示例中省略了 building 标志(低估了它的重要性)。已编辑我的答案。 - Matthias Meid
感谢这个答案,因为它是一个学习场景。我经常看到人们不使用单独的Builder类来实现不可变副本而直接使用Builder模式。因此,这是我看到人们使用的Builder模式的一种常见变体。但我个人不喜欢它。这基本上意味着在获取构建好的副本之后,你仍然可以一遍又一遍地调用.build方法。这太不合逻辑了。 - djangofan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接